diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7d95099ce985..d5a9291e707b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -38,10 +38,10 @@ /components/ @azhavoro # Component: Tests -/tests/ @yasakova-anastasia +/tests/ @kirill-sizov # Component: Serverless functions -/serverless/ @yasakova-anastasia +/serverless/ @kirill-sizov # Infrastructure Dockerfile* @azhavoro diff --git a/.github/codecov.yml b/.github/codecov.yml index d259debd2cb1..aa7fd1c08451 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -23,5 +23,11 @@ codecov: require_ci_to_pass: yes notify: wait_for_ci: yes - after_n_builds: 16 +coverage: + status: + patch: false + project: + default: + target: auto + threshold: 5% \ No newline at end of file diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml index 2bdf459a568e..cab6e202e484 100644 --- a/.github/workflows/bandit.yml +++ b/.github/workflows/bandit.yml @@ -23,19 +23,11 @@ jobs: . .env/bin/activate pip install -U pip wheel setuptools pip install bandit - mkdir -p bandit_report echo "Bandit version: "$(bandit --version | head -1) echo "The files will be checked: "$(echo $CHANGED_FILES) - bandit -a file --ini .bandit -f html -o ./bandit_report/bandit_checks.html $CHANGED_FILES + bandit -a file --ini .bandit $CHANGED_FILES deactivate else echo "No files with the \"py\" extension found" fi - - - name: Upload artifacts - if: failure() - uses: actions/upload-artifact@v3.1.1 - with: - name: bandit_report - path: bandit_report diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 1fa9da8a9634..460dc102e044 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -13,6 +13,7 @@ jobs: cvat-cli/**/*.py tests/python/**/*.py cvat/apps/quality_control/**/*.py + cvat/apps/analytics_report/**/*.py dir_names: true - name: Run checks @@ -32,23 +33,15 @@ jobs: . .env/bin/activate pip install -U pip wheel setuptools pip install $(egrep "black.*" ./cvat-cli/requirements/development.txt) - mkdir -p black_report echo "Black version: "$(black --version) echo "The dirs will be checked: $UPDATED_DIRS" EXIT_CODE=0 for DIR in $UPDATED_DIRS; do - black --check --diff $DIR >> ./black_report/black_checks.txt || EXIT_CODE=$(($? | $EXIT_CODE)) || true + black --check --diff $DIR || EXIT_CODE=$(($? | $EXIT_CODE)) || true done deactivate exit $EXIT_CODE else echo "No files with the \"py\" extension found" fi - - - name: Upload artifacts - if: failure() - uses: actions/upload-artifact@v3.1.1 - with: - name: black_report - path: black_report diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 21576df62977..c823f40fdc74 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -13,17 +13,8 @@ jobs: run: | yarn install --frozen-lockfile (cd tests && yarn install --frozen-lockfile) - yarn add eslint-detailed-reporter -D -W - name: Run checks run: | echo "ESLint version: "$(yarn run -s eslint --version) - mkdir -p eslint_report - yarn run eslint . -f node_modules/eslint-detailed-reporter/lib/detailed.js -o ./eslint_report/eslint_checks.html - - - name: Upload artifacts - if: failure() - uses: actions/upload-artifact@v3.1.1 - with: - name: eslint_report - path: eslint_report + yarn run eslint . diff --git a/.github/workflows/full.yml b/.github/workflows/full.yml index 598245b9450d..d2f0a23a3c32 100644 --- a/.github/workflows/full.yml +++ b/.github/workflows/full.yml @@ -52,19 +52,12 @@ jobs: - name: CVAT server. Build and push uses: docker/build-push-action@v3 with: - build-args: | - "COVERAGE_PROCESS_START=.coveragerc" cache-from: type=local,src=/tmp/cvat_cache_server context: . file: Dockerfile tags: cvat/server outputs: type=docker,dest=/tmp/cvat_server/image.tar - - name: Instrumentation of the code then rebuilding the CVAT UI - run: | - yarn --frozen-lockfile - yarn run coverage - - name: CVAT UI. Build and push uses: docker/build-push-action@v3 with: @@ -161,8 +154,6 @@ jobs: - name: Running REST API and SDK tests id: run_tests - env: - COVERAGE_PROCESS_START: ".coveragerc" run: | pip3 install -r cvat-sdk/gen/requirements.txt ./cvat-sdk/gen/generate.sh @@ -171,12 +162,7 @@ jobs: pip3 install -e ./cvat-sdk pip3 install -e ./cvat-cli - pytest tests/python/ --cov --cov-report xml - - - name: Upload coverage reports to Codecov with GitHub Action - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} + pytest tests/python/ - name: Creating a log file from cvat containers if: failure() && steps.run_tests.conclusion == 'failure' @@ -236,15 +222,10 @@ jobs: while [[ $(curl -s -o /dev/null -w "%{http_code}" localhost:8181/health?bundles) != "200" && max_tries -gt 0 ]]; do (( max_tries-- )); sleep 5; done docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash \ - -c 'coverage run -a manage.py test cvat/apps && coverage json && mv coverage.json ${CONTAINER_COVERAGE_DATA_DIR}' + -c 'python manage.py test cvat/apps -v 2' docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash \ - -c 'yarn --frozen-lockfile --ignore-scripts && yarn workspace cvat-core run test && mv cvat-core/reports/coverage/coverage-final.json ${CONTAINER_COVERAGE_DATA_DIR}' - - - name: Upload coverage reports to Codecov with GitHub Action - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} + -c 'yarn --frozen-lockfile --ignore-scripts && yarn workspace cvat-core run test' - name: Creating a log file from cvat containers if: failure() @@ -340,19 +321,16 @@ jobs: npx cypress run \ --headed \ --browser chrome \ + --env coverage=false \ --config-file cypress_canvas3d.config.js \ --spec 'cypress/e2e/${{ matrix.specs }}/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js' else npx cypress run \ --browser chrome \ + --env coverage=false \ --spec 'cypress/e2e/${{ matrix.specs }}/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js' fi - - name: Upload coverage reports to Codecov with GitHub Action - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - - name: Creating a log file from "cvat" container logs if: failure() run: | diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index aa81751ab536..5978375a8654 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -14,7 +14,7 @@ jobs: - name: Run checks env: HADOLINT: "${{ github.workspace }}/hadolint" - HADOLINT_VER: "2.1.0" + HADOLINT_VER: "2.12.0" VERIFICATION_LEVEL: "error" run: | CHANGED_FILES="${{steps.files.outputs.all_changed_files}}" @@ -23,26 +23,8 @@ jobs: curl -sL -o $HADOLINT "https://github.com/hadolint/hadolint/releases/download/v$HADOLINT_VER/hadolint-Linux-x86_64" && chmod 700 $HADOLINT echo "HadoLint version: "$($HADOLINT --version) echo "The files will be checked: "$(echo $CHANGED_FILES) - mkdir -p hadolint_report - $HADOLINT --no-fail --format json $CHANGED_FILES > ./hadolint_report/hadolint_report.json - GET_VERIFICATION_LEVEL=$(cat ./hadolint_report/hadolint_report.json | jq -r '.[] | .level') - for LINE in $GET_VERIFICATION_LEVEL; do - if [[ $LINE =~ $VERIFICATION_LEVEL ]]; then - pip install json2html - python ./tests/json_to_html.py ./hadolint_report/hadolint_report.json - exit 1 - else - exit 0 - fi - done + $HADOLINT -t $VERIFICATION_LEVEL $CHANGED_FILES else echo "No files with the \"Dockerfile*\" name found" fi - - - name: Upload artifacts - if: failure() - uses: actions/upload-artifact@v3.1.1 - with: - name: hadolint_report - path: hadolint_report diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index d90c33fb6087..ffbde729a86e 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -5,7 +5,7 @@ on: - 'master' - 'develop' pull_request: - types: [edited, ready_for_review, opened, synchronize, reopened] + types: [ready_for_review, opened, synchronize, reopened] paths-ignore: - 'site/**' - '**/*.md' @@ -44,11 +44,11 @@ jobs: - name: Deploy to minikube run: | - printf " service:\n externalIPs:\n - $(minikube ip)\n" >> tests/values.test.yaml + printf " service:\n externalIPs:\n - $(minikube ip)\n" >> helm-chart/test.values.yaml cd helm-chart helm dependency update cd .. - helm upgrade -n default release-${{ github.run_id }}-${{ github.run_attempt }} -i --create-namespace helm-chart -f helm-chart/values.yaml -f tests/values.test.yaml + helm upgrade -n default release-${{ github.run_id }}-${{ github.run_attempt }} -i --create-namespace helm-chart -f helm-chart/values.yaml -f helm-chart/cvat.values.yaml -f helm-chart/test.values.yaml - name: Update test config run: | diff --git a/.github/workflows/isort.yml b/.github/workflows/isort.yml index 87e2d9957b28..f3157b446c75 100644 --- a/.github/workflows/isort.yml +++ b/.github/workflows/isort.yml @@ -13,6 +13,7 @@ jobs: cvat-cli/**/*.py tests/python/**/*.py cvat/apps/quality_control/**/*.py + cvat/apps/analytics_report/**/*.py dir_names: true - name: Run checks @@ -29,23 +30,15 @@ jobs: . .env/bin/activate pip install -U pip wheel setuptools pip install $(egrep "isort.*" ./cvat-cli/requirements/development.txt) - mkdir -p isort_report echo "isort version: $(isort --version-number)" echo "The dirs will be checked: $UPDATED_DIRS" EXIT_CODE=0 for DIR in $UPDATED_DIRS; do - isort --check $DIR >> ./isort_report/isort_checks.txt || EXIT_CODE=$(($? | $EXIT_CODE)) || true + isort --check $DIR || EXIT_CODE=$(($? | $EXIT_CODE)) || true done deactivate exit $EXIT_CODE else echo "No files with the \"py\" extension found" fi - - - name: Upload artifacts - if: failure() - uses: actions/upload-artifact@v3.1.1 - with: - name: isort_report - path: isort_report diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 656e382bcb30..023645d52380 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -167,12 +167,14 @@ jobs: env: COVERAGE_PROCESS_START: ".coveragerc" run: | - pytest tests/python/ --cov --cov-report xml + pytest tests/python/ --cov --cov-report=json - - name: Upload coverage reports to Codecov with GitHub Action - uses: codecov/codecov-action@v3 + - name: Uploading code coverage results as an artifact + uses: actions/upload-artifact@v3.1.1 with: - token: ${{ secrets.CODECOV_TOKEN }} + name: coverage_results + path: | + coverage*.json - name: Creating a log file from cvat containers if: failure() && steps.run_tests.conclusion == 'failure' @@ -230,15 +232,18 @@ jobs: while [[ $(curl -s -o /dev/null -w "%{http_code}" localhost:8181/health?bundles) != "200" && max_tries -gt 0 ]]; do (( max_tries-- )); sleep 5; done docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash \ - -c 'coverage run -a manage.py test cvat/apps && coverage json && mv coverage.json ${CONTAINER_COVERAGE_DATA_DIR}' + -c 'coverage run -a manage.py test cvat/apps && coverage json && mv coverage.json ${CONTAINER_COVERAGE_DATA_DIR}/unit_tests_coverage.json' docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash \ -c 'yarn --frozen-lockfile --ignore-scripts && yarn workspace cvat-core run test && mv cvat-core/reports/coverage/coverage-final.json ${CONTAINER_COVERAGE_DATA_DIR}' - - name: Upload coverage reports to Codecov with GitHub Action - uses: codecov/codecov-action@v3 + - name: Uploading code coverage results as an artifact + uses: actions/upload-artifact@v3.1.1 with: - token: ${{ secrets.CODECOV_TOKEN }} + name: coverage_results + path: | + ${{ github.workspace }}/coverage-final.json + ${{ github.workspace }}/unit_tests_coverage.json - name: Creating a log file from cvat containers if: failure() @@ -339,11 +344,14 @@ jobs: --browser chrome \ --spec 'cypress/e2e/${{ matrix.specs }}/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js' fi + mv coverage/coverage-final.json coverage/${{ matrix.specs }}_coverage.json - - name: Upload coverage reports to Codecov with GitHub Action - uses: codecov/codecov-action@v3 + - name: Uploading code coverage results as an artifact + uses: actions/upload-artifact@v3.1.1 with: - token: ${{ secrets.CODECOV_TOKEN }} + name: coverage_results + path: | + tests/coverage/${{ matrix.specs }}_coverage.json - name: Creating a log file from "cvat" container logs if: failure() @@ -463,3 +471,19 @@ jobs: docker tag cvat/ui:latest "${UI_IMAGE_REPO}:dev" docker push "${UI_IMAGE_REPO}:dev" + + codecov: + runs-on: ubuntu-latest + needs: [unit_testing, e2e_testing, rest_api_testing] + steps: + - uses: actions/checkout@v3 + + - name: Downloading coverage results + uses: actions/download-artifact@v3 + with: + name: coverage_results + + - name: Upload coverage reports to Codecov with GitHub Action + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index ca4de7106ba0..f54623bc2984 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -23,24 +23,13 @@ jobs: python3 -m venv .env . .env/bin/activate pip install -U pip wheel setuptools - pip install pylint-json2html pip install $(egrep "pylint.*==.*" ./cvat/requirements/development.txt) pip install $(egrep "django==.*" ./cvat/requirements/base.txt) - mkdir -p pylint_report echo "Pylint version: "$(pylint --version | head -1) echo "The files will be checked: "$(echo $CHANGED_FILES) - pylint $CHANGED_FILES --output-format=json > ./pylint_report/pylint_checks.json || EXIT_CODE=$(echo $?) || true - pylint-json2html -o ./pylint_report/pylint_checks.html ./pylint_report/pylint_checks.json + pylint $CHANGED_FILES deactivate - exit $EXIT_CODE else echo "No files with the \"py\" extension found" fi - - - name: Upload artifacts - if: failure() - uses: actions/upload-artifact@v3.1.1 - with: - name: pylint_report - path: pylint_report diff --git a/.github/workflows/remark.yml b/.github/workflows/remark.yml index 8524364a5746..86e06962013b 100644 --- a/.github/workflows/remark.yml +++ b/.github/workflows/remark.yml @@ -12,20 +12,6 @@ jobs: - name: Run checks run: | yarn install --frozen-lockfile - mkdir -p remark_report echo "Remark version: "`npx remark --version` - npx remark --quiet --report json --no-stdout -i .remarkignore . 2> ./remark_report/remark_report.json - get_report=`cat ./remark_report/remark_report.json | jq -r '.[] | select(.messages | length > 0)'` - if [[ ! -z ${get_report} ]]; then - pip install json2html - python ./tests/json_to_html.py ./remark_report/remark_report.json - exit 1 - fi - - - name: Upload artifacts - if: failure() - uses: actions/upload-artifact@v3.1.1 - with: - name: remark_report - path: remark_report + npx remark --quiet --frail -i .remarkignore . diff --git a/.github/workflows/schedule.yml b/.github/workflows/schedule.yml index 80808b29159e..af521ccae138 100644 --- a/.github/workflows/schedule.yml +++ b/.github/workflows/schedule.yml @@ -88,8 +88,6 @@ jobs: - name: CVAT server. Build and push uses: docker/build-push-action@v3 with: - build-args: | - "COVERAGE_PROCESS_START=.coveragerc" cache-from: type=local,src=/tmp/cvat_cache_server context: . file: Dockerfile @@ -97,11 +95,6 @@ jobs: tags: ${{ steps.meta-server.outputs.tags }} labels: ${{ steps.meta-server.outputs.labels }} - - name: Instrumentation of the code then rebuilding the CVAT UI - run: | - yarn --frozen-lockfile - yarn run coverage - - name: CVAT UI. Build and push uses: docker/build-push-action@v3 with: @@ -165,8 +158,6 @@ jobs: ./opa test cvat/apps/iam/rules - name: REST API and SDK tests - env: - COVERAGE_PROCESS_START: ".coveragerc" run: | pip3 install -r cvat-sdk/gen/requirements.txt ./cvat-sdk/gen/generate.sh @@ -175,7 +166,7 @@ jobs: pip3 install -e ./cvat-sdk pip3 install -e ./cvat-cli - pytest tests/python/ --cov --cov-report xml + pytest tests/python/ pytest tests/python/ --stop-services - name: Unit tests @@ -188,18 +179,13 @@ jobs: while [[ $(curl -s -o /dev/null -w "%{http_code}" localhost:8181/health?bundles) != "200" && max_tries -gt 0 ]]; do (( max_tries-- )); sleep 5; done docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash \ - -c 'coverage run -a manage.py test cvat/apps && coverage json && mv coverage.json ${CONTAINER_COVERAGE_DATA_DIR}' + -c 'python manage.py test cvat/apps -v 2' docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash \ - -c 'yarn --frozen-lockfile --ignore-scripts && yarn workspace cvat-core run test && mv cvat-core/reports/coverage/coverage-final.json ${CONTAINER_COVERAGE_DATA_DIR}' + -c 'yarn --frozen-lockfile --ignore-scripts && yarn workspace cvat-core run test' docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml down -v - - name: Upload coverage reports to Codecov with GitHub Action - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - e2e_testing: needs: build runs-on: ubuntu-latest @@ -304,19 +290,16 @@ jobs: npx cypress run \ --headed \ --browser chrome \ + --env coverage=false \ --config-file cypress_canvas3d.config.js \ --spec 'cypress/e2e/${{ matrix.specs }}/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js' else npx cypress run \ --browser chrome \ + --env coverage=false \ --spec 'cypress/e2e/${{ matrix.specs }}/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js' fi - - name: Upload coverage reports to Codecov with GitHub Action - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - - name: Creating a log file from "cvat" container logs if: failure() run: | diff --git a/.github/workflows/stylelint.yml b/.github/workflows/stylelint.yml index 3fdfd81cf26e..e47e64fab246 100644 --- a/.github/workflows/stylelint.yml +++ b/.github/workflows/stylelint.yml @@ -21,21 +21,10 @@ jobs: if [[ ! -z $CHANGED_FILES ]]; then yarn install --frozen-lockfile - mkdir -p stylelint_report echo "StyleLint version: "$(npx stylelint --version) echo "The files will be checked: "$(echo $CHANGED_FILES) - npx stylelint --formatter json --output-file ./stylelint_report/stylelint_report.json $CHANGED_FILES || EXIT_CODE=$(echo $?) || true - pip install json2html - python ./tests/json_to_html.py ./stylelint_report/stylelint_report.json - exit $EXIT_CODE + npx stylelint $CHANGED_FILES else echo "No files with the \"css|scss\" extension found" fi - - - name: Upload artifacts - if: failure() - uses: actions/upload-artifact@v3.1.1 - with: - name: stylelint_report - path: stylelint_report diff --git a/.stylelintrc.json b/.stylelintrc.json index 44333abf546d..f16c1b3e7d7e 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,22 +1,11 @@ { - "extends": "stylelint-config-standard", + "extends": "stylelint-config-standard-scss", "rules": { - "indentation": 4, + "scss/comment-no-empty": null, "value-keyword-case": null, - "selector-combinator-space-after": null, - "no-descending-specificity": null, - "at-rule-no-unknown": [ - true, - { - "ignoreAtRules": ["extend"] - } - ], - "selector-type-no-unknown": [ - true, - { - "ignoreTypes": ["first-child"] - } - ] + "color-function-notation": ["legacy"], + "scss/at-extend-no-missing-placeholder": null, + "no-descending-specificity": null }, "ignoreFiles": ["**/*.js", "**/*.ts", "**/*.py"] } diff --git a/.vscode/launch.json b/.vscode/launch.json index a9e3d672c97b..366bd49a3fa1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -104,6 +104,26 @@ ], "justMyCode": false, }, + { + "name": "REST API tests: Attach to RQ analytics reports worker", + "type": "python", + "request": "attach", + "connect": { + "host": "127.0.0.1", + "port": 9095 + }, + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "/home/django/" + }, + { + "localRoot": "${workspaceFolder}/.env", + "remoteRoot": "/opt/venv", + } + ], + "justMyCode": false, + }, { "type": "pwa-chrome", "request": "launch", @@ -240,6 +260,28 @@ }, "console": "internalConsole" }, + { + "name": "server: RQ - analytics reports", + "type": "python", + "request": "launch", + "stopOnEntry": false, + "justMyCode": false, + "python": "${command:python.interpreterPath}", + "program": "${workspaceRoot}/manage.py", + "args": [ + "rqworker", + "analytics_reports", + "--worker-class", + "cvat.rqworker.SimpleWorker", + ], + "django": true, + "cwd": "${workspaceFolder}", + "env": { + "DJANGO_LOG_SERVER_HOST": "localhost", + "DJANGO_LOG_SERVER_PORT": "8282" + }, + "console": "internalConsole" + }, { "name": "server: RQ - scheduler", "type": "python", @@ -499,6 +541,7 @@ "server: RQ - webhooks", "server: RQ - scheduler", "server: RQ - quality reports", + "server: RQ - analytics reports", "server: RQ - cleaning", "server: git", ] diff --git a/CHANGELOG.md b/CHANGELOG.md index d3848f0a14d0..933d5468e80d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,29 +5,77 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## \[2.5.2\] - 2023-07-27 + +### Added + +- We've added support for multi-line text attributes () +- You can now set a default attribute value for SELECT, RADIO types on UI + () +- \[SDK\] `cvat_sdk.datasets`, is now available, providing a framework-agnostic alternative to `cvat_sdk.pytorch` + () +- We've introduced analytics for Jobs, Tasks, and Project () + +### Changed + +- \[Helm\] In Helm, we've added a configurable default storage option to the chart () + +### Removed + +- \[Helm\] In Helm, we've eliminated the obligatory use of hardcoded traefik ingress () + +### Fixed + +- Fixed an issue with calculating the number of objects on the annotation view when frames are deleted + () +- \[SDK\] In SDK, we've fixed the issue with creating attributes with blank default values + () +- \[SDK\] We've corrected a problem in SDK where it was altering input data in models () +- Fixed exporting of hash for shapes and tags in a specific corner case () +- Resolved the issue where 3D jobs couldn't be opened in validation mode () +- Fixed SAM plugin (403 code for workers in organizations) () +- Fixed the issue where initial frame from query parameter was not opening specific frame in a job + () +- Corrected the issue with the removal of the first keyframe () +- Fixed the display of project previews on small screens and updated stylelint & rules () +- Implemented server-side validation for attribute specifications + () +- \[API\] Fixed API issue related to file downloading failures for filenames with special characters () +- \[Helm\] In Helm, we've resolved an issue with multiple caches + in the same RWX volume, which was preventing db migration from starting () + ## \[2.5.1\] - 2023-07-19 + ### Fixed + - Memory leak related to unclosed av container () ## \[2.5.0] - 2023-07-05 + ### Added + - Now CVAT supports project/task markdown description with additional assets (png, jpeg, gif, webp images and pdf files) () - Ground Truth jobs and quality analytics for tasks () ### Fixed + - The problem with manifest file in tasks restored from backup () - The problem with task mode in a task restored from backup () - Visible 'To background' button in review mode () - Added missed auto_add argument to Issue model () - \[API\] Performance of several API endpoints () - \[API\] Invalid schema for the owner field in several endpoints () +- Some internal errors occurring during lambda function invocations + could be mistakenly reported as invalid requests + () - \[SDK\] Loading tasks that have been cached with the PyTorch adapter () - The problem with importing annotations if dataset has extra dots in filenames () ### Security + - More comprehensive SSRF mitigations were implemented. Previously, on task creation it was prohibited to specify remote data URLs with hosts that resolved to IP addresses in the private ranges. @@ -38,56 +86,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (). ## \[2.4.9] - 2023-06-22 + ### Fixed + - Error related to calling serverless functions on some image formats () ## \[2.4.8] - 2023-06-22 + ### Fixed + - Getting original chunks for items in specific cases () ## \[2.4.7] - 2023-06-16 + ### Added + - \[API\] API Now supports the creation and removal of Ground Truth jobs. () - \[API\] We've introduced task quality estimation endpoints. () - \[CLI\] An option to select the organization. () ### Fixed + - Issues with running serverless models for EXIF-rotated images. () - File uploading issues when using https configuration. () - Dataset export error with `outside` property of tracks. () - Broken logging in the TransT serverless function. () ## \[2.4.6] - 2023-06-09 + ### Added + - \[Server API\] An option to supply custom file ordering for task data uploads () -- New option ``semi-auto`` is available as annotations source () +- New option `semi-auto` is available as annotations source () ### Changed + - Allowed to use dataset manifest for the `predefined` sorting method for task data () ### Changed + - Replaced Apache mod_wsgi with Uvicorn ASGI server for backend use() ### Fixed + - Incorrect location of temporary file during job annotation import.() - Deletion of uploaded file along with annotations/backups when an RQ job has been initiated, but no subsequent status check requests have been made.() - Deletion of uploaded files, including annotations and backups, - after they have been uploaded to the server using the TUS protocol but before an RQ job has been initiated. () + after they have been uploaded to the server using the TUS protocol but before an RQ job has been initiated. () - Simultaneous creation of tasks or projects with identical names from backups by multiple users.() - \[API\] The `predefined` sorting method for task data uploads () - Allowed slashes in export filenames. () ## \[2.4.5] - 2023-06-02 + ### Added + - Integrated support for sharepoint and cloud storage files, along with directories to be omitted during task creation (server) () -- Enabled task creation with directories from cloud storage or sharepoint () +- Enabled task creation with directories from cloud storage or sharepoint () - Enhanced task creation to support any data type supported by the server - by default, from cloud storage without the necessity for the `use_cache` option () -- Added capability for task creation with data from cloud storage without the `use_cache` option () + by default, from cloud storage without the necessity for the `use_cache` option () +- Added capability for task creation with data from cloud storage without the `use_cache` option () ### Changed + - User can now access resource links from any organization or sandbox, granted it's available to them () - Cloud storage manifest files have been made optional () - Updated Django to the 4.2.x version () @@ -96,18 +159,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 () ### Deprecated + - Deprecated the endpoint `/cloudstorages/{id}/content` () ### Fixed + - Fixed the issue of skeletons dumping on created tasks/projects () - Resolved an issue related to saving annotations for skeleton tracks () ## \[2.4.4] - 2023-05-18 + ### Added + - Introduced a new configuration option for controlling the invocation of Nuclio functions. () ### Changed + - Relocated SAM masks decoder to frontend operation. () - Switched `person-reidentification-retail-0300` and `faster_rcnn_inception_v2_coco` Nuclio functions with `person-reidentification-retail-0277` and `faster_rcnn_inception_resnet_v2_atrous_coco` respectively. @@ -116,6 +184,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 () ### Fixed + - Resolved issues with tracking multiple objects (30 and more) using the TransT tracker. () - Addressed azure.core.exceptions.ResourceExistsError: The specified blob already exists. @@ -134,26 +203,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 () ## \[2.4.3] - 2023-04-24 + ### Changed + - Docker images no longer include Ubuntu package sources or FFmpeg/OpenH264 sources () - TUS chunk size changed from 100 MB to 2 MB () ## \[2.4.2] - 2023-04-14 + ### Added + - Support for Azure Blob Storage connection string authentication() - Segment Anything interactor for CPU/GPU () ### Changed + - The capability to transfer a task from one project to another project has been disabled () - The bounding rectangle in the skeleton annotation is visible solely when the skeleton is active () - Base backend image upgraded from ubuntu:20.04 to ubuntu:22.04 () ### Deprecated + - TDB ### Removed + - Cloud storage `unique_together` limitation () - Support for redundant request media types in the API () @@ -163,6 +239,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 () ### Fixed + - An invalid project/org handling in webhooks () - Warning `key` is undefined on project page () - An invalid mask detected when performing automatic annotation on a task () @@ -188,16 +265,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The issue related to webhook events not being sent has been resolved () ### Security + - Updated Redis (in the Compose file) to 7.0.x, and redis-py to 4.5.4 () ## \[2.4.1] - 2023-04-05 + ### Fixed + - Optimized annotation fetching up to 10 times () - Incorrect calculation of working time in analytics () ## \[2.4.0] - 2023-03-16 + ### Added + - \[SDK\] An arg to wait for data processing in the task data uploading function () - Filename pattern to simplify uploading cloud storage data for a task (, ) @@ -207,7 +289,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 () - Grid view and multiple context images supported () - Interpolation is now supported for 3D cuboids. -Tracks can be exported/imported to/from Datumaro and Sly Pointcloud formats () +- Tracks can be exported/imported to/from Datumaro and Sly Pointcloud formats () - Support for custom file to job splits in tasks (server API & SDK only) () - \[SDK\] A PyTorch adapter setting to disable cache updates @@ -223,6 +305,7 @@ Tracks can be exported/imported to/from Datumaro and Sly Pointcloud formats () ### Changed + - The Docker Compose files now use the Compose Specification version of the format. This version is supported by Docker Compose 1.27.0+ (). @@ -246,9 +329,11 @@ Tracks can be exported/imported to/from Datumaro and Sly Pointcloud formats () ### Deprecated + - TBD ### Removed + - \[Server API\] Endpoints with collections are removed in favor of their full variants `/project/{id}/tasks`, `/tasks/{id}/jobs`, `/jobs/{id}/issues`, `/issues/{id}/comments`. Corresponding fields are added or changed to provide a link to the child collection @@ -257,6 +342,7 @@ Tracks can be exported/imported to/from Datumaro and Sly Pointcloud formats () ### Fixed + - Helm: Empty password for Redis () - Resolved HRNet serverless function runtime error on images with an alpha channel () - Addressed ignored preview & chunk cache settings () @@ -276,31 +362,35 @@ Tracks can be exported/imported to/from Datumaro and Sly Pointcloud formats () ### Security + - Fixed vulnerability with social authentication () ## \[2.3.0] - 2022-12-22 + ### Added + - SDK section in documentation () - Option to enable or disable host certificate checking in CLI () - REST API tests with skeletons () - Host schema auto-detection in SDK () - Server compatibility checks in SDK () - Objects sorting option in the sidebar, by z-order. Additional visualization when sorting is applied -() + () - Added YOLOv5 serverless function with NVIDIA GPU support () - Mask tools now supported (brush, eraser, polygon-plus, -polygon-minus, returning masks from online detectors & interactors) -() + polygon-minus, returning masks from online detectors & interactors) + () - Added Webhooks () - Authentication with social accounts: Google & GitHub (, , ) - REST API tests for exporting job datasets & annotations and validating their structure () - Backward propagation on UI () - Keyboard shortcut to delete a frame (Alt + Del) () - PyTorch dataset adapter layer in the SDK -() + () - Method for debugging the server deployed with Docker () ### Changed + - `api/docs`, `api/swagger`, `api/schema`, `server/about` endpoints now allow unauthorized access (, ) - 3D canvas now can be dragged in IDLE mode () - Datumaro version is upgraded to 0.3 (dev) () @@ -312,14 +402,16 @@ polygon-minus, returning masks from online detectors & interactors) () ### Removed + - The `--https` option of CLI () ### Fixed + - Significantly optimized access to DB for api/jobs, api/tasks, and api/projects. - Removed a possibly duplicated encodeURI() calls in `server-proxy.ts` to prevent doubly encoding -non-ascii paths while adding files from "Connected file share" (issue #4428) + non-ascii paths while adding files from "Connected file share" (issue #4428) - Removed unnecessary volumes defined in docker-compose.serverless.yml -() + () - Added support for Image files that use the PIL.Image.mode 'I;16' - Project import/export with skeletons (, ) @@ -327,7 +419,7 @@ non-ascii paths while adding files from "Connected file share" (issue #4428) - Unstable e2e restore tests () - IOG and f-BRS serverless function () - Invisible label item in label constructor when label color background is white, - or close to it () + or close to it () - Fixed cvat-core ESlint problems () - Fixed task creation with non-local files via the SDK/CLI () @@ -338,11 +430,11 @@ non-ascii paths while adding files from "Connected file share" (issue #4428) - Double modal export/backup a task/project () - Fixed bug of computing Job's unsolved/resolved issues numbers () - Dataset export for job () -- Angle is not propagated when use ``propagate`` feature () +- Angle is not propagated when use `propagate` feature () - Could not fetch task in a corner case () - Restoring CVAT in case of React-renderning fail () - Deleted frames become restored if a user deletes frames from another job of the same task -() + () - Wrong issue position when create a quick issue on a rotated shape () - Extra rerenders of different pages with each click () - Skeleton points exported out of order in the COCO Keypoints format @@ -382,11 +474,14 @@ non-ascii paths while adding files from "Connected file share" (issue #4428) - Skeletons cannot be added to a task or project () ### Security + - `Project.import_dataset` not waiting for completion correctly () ## \[2.2.0] - 2022-09-12 + ### Added + - Added ability to delete frames from a job based on () - Support of attributes returned by serverless functions based on () - Project/task backups uploading via chunk uploads @@ -403,7 +498,7 @@ non-ascii paths while adding files from "Connected file share" (issue #4428) - OpenCV.js caching and autoload () - Publishing dev version of CVAT docker images () - Support of Human Pose Estimation, Facial Landmarks (and similar) use-cases, new shape type: -Skeleton (), () +- Skeleton (), () - Added helm chart support for serverless functions and analytics () - Added confirmation when remove a track () - [COCO Keypoints](https://cocodataset.org/#keypoints-2020) format support (, @@ -415,6 +510,7 @@ Skeleton (), () ### Changed + - Bumped nuclio version to 1.8.14 - Simplified running REST API tests. Extended CI-nightly workflow - REST API tests are partially moved to Python SDK (`users`, `projects`, `tasks`, `issues`) @@ -423,8 +519,9 @@ Skeleton (), (), () ## \[2.1.0] - 2022-04-08 + ### Added + - Task annotations importing via chunk uploads () - Advanced filtration and sorting for a list of tasks/projects/cloudstorages () - Project dataset importing via chunk uploads () - Support paginated list for job commits () ### Changed + - Added missing geos dependency into Dockerfile () - Improved helm chart readme () - Added helm chart support for CVAT 2.X and made ingress compatible with Kubernetes >=1.22 () ### Fixed + - Permission error occurred when accessing the JobCommits () - job assignee can remove or update any issue created by the task owner () - Bug: Incorrect point deletion with keyboard shortcut () @@ -466,7 +567,9 @@ Skeleton (), () ## \[2.0.0] - 2022-03-04 + ### Added + - Handle attributes coming from nuclio detectors () - Add additional environment variables for Nuclio configuration () - Add KITTI segmentation and detection format () @@ -499,6 +602,7 @@ Skeleton (), () ### Changed + - Users don't have access to a task object anymore if they are assigned only on some jobs of the task () - Different resources (tasks, projects) are not visible anymore for all CVAT instance users by default () - API versioning scheme: using accept header versioning instead of namespace versioning () @@ -507,14 +611,17 @@ Skeleton (), () ### Deprecated + - Job field "status" is not used in UI anymore, but it has not been removed from the database yet () ### Removed + - Review rating, reviewer field from the job instance (use assignee field together with stage field instead) () - Training django app () - v1 api version support () ### Fixed + - Fixed Interaction handler keyboard handlers () - Points of invisible shapes are visible in autobordering () - Order of the label attributes in the object item details() @@ -541,8 +648,8 @@ Skeleton (), () - Bug: Permission error occurred when accessing the comments of a specific issue () - ### Security + - Updated ELK to 6.8.23 which uses log4j 2.17.1 () - Added validation for URLs which used as remote data source () diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index 68b60997cca0..64ac4c6e81f7 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.17.0", + "version": "2.17.1", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index fa886c872831..1a6d442d614e 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -2183,7 +2183,7 @@ export class CanvasViewImpl implements CanvasView, Listener { } private deactivateShape(): void { - if (this.activeElement.clientID !== null) { + if (this.activeElement.clientID) { const { displayAllText } = this.configuration; const { clientID } = this.activeElement; const drawnState = this.drawnStates[clientID]; @@ -2667,9 +2667,20 @@ export class CanvasViewImpl implements CanvasView, Listener { }); } - for (const tspan of (text.lines() as any).members) { - tspan.attr('x', text.attr('x')); + function applyParentX(parentText: SVGTSpanElement | SVGTextElement): void { + for (let i = 0; i < parentText.children.length; i++) { + if (i === 0) { + // do not align the first child + continue; + } + + const tspan = parentText.children[i]; + tspan.setAttribute('x', parentText.getAttribute('x')); + applyParentX(tspan as SVGTSpanElement); + } } + + applyParentX(text.node as any as SVGTextElement); } private deleteText(clientID: number): void { @@ -2733,15 +2744,16 @@ export class CanvasViewImpl implements CanvasView, Listener { } if (withAttr) { for (const attrID of Object.keys(attributes)) { - const value = attributes[attrID] === undefinedAttrValue ? '' : attributes[attrID]; - block - .tspan(`${attrNames[attrID]}: ${value}`) - .attr({ - attrID, - dy: '1em', - x: 0, - }) - .addClass('cvat_canvas_text_attribute'); + const values = `${attributes[attrID] === undefinedAttrValue ? '' : attributes[attrID]}`.split('\n'); + const parent = block.tspan(`${attrNames[attrID]}: `) + .attr({ attrID, dy: '1em', x: 0 }).addClass('cvat_canvas_text_attribute'); + values.forEach((attrLine: string, index: number) => { + parent + .tspan(attrLine) + .attr({ + dy: index === 0 ? 0 : '1em', + }); + }); } } }) diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts index 75d69be5ac75..9e89616a86f5 100644 --- a/cvat-canvas/src/typescript/shared.ts +++ b/cvat-canvas/src/typescript/shared.ts @@ -41,7 +41,7 @@ export interface DrawnState { occluded?: boolean; hidden?: boolean; lock: boolean; - source: 'AUTO' | 'SEMI-AUTO' | 'MANUAL'; + source: 'AUTO' | 'SEMI-AUTO' | 'MANUAL' | 'FILE'; shapeType: string; points?: number[]; rotation: number; diff --git a/cvat-canvas/webpack.config.js b/cvat-canvas/webpack.config.js index 9475dd51b345..382e3754d3ef 100644 --- a/cvat-canvas/webpack.config.js +++ b/cvat-canvas/webpack.config.js @@ -19,7 +19,13 @@ const styleLoaders = [ { loader: 'postcss-loader', options: { - plugins: [require('postcss-preset-env')], + postcssOptions: { + plugins: [ + [ + 'postcss-preset-env', {}, + ], + ], + }, }, }, 'sass-loader', diff --git a/cvat-canvas3d/webpack.config.js b/cvat-canvas3d/webpack.config.js index 5c8b41b25c41..f3c7bd789a8b 100644 --- a/cvat-canvas3d/webpack.config.js +++ b/cvat-canvas3d/webpack.config.js @@ -19,7 +19,13 @@ const styleLoaders = [ { loader: 'postcss-loader', options: { - plugins: [require('postcss-preset-env')], + postcssOptions: { + plugins: [ + [ + 'postcss-preset-env', {}, + ], + ], + }, }, }, 'sass-loader', diff --git a/cvat-cli/requirements/base.txt b/cvat-cli/requirements/base.txt index ecef7b5601f1..612076fc5c72 100644 --- a/cvat-cli/requirements/base.txt +++ b/cvat-cli/requirements/base.txt @@ -1,3 +1,3 @@ -cvat-sdk~=2.4.0 +cvat-sdk~=2.5.0 Pillow>=6.2.0 setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/cvat-cli/src/cvat_cli/version.py b/cvat-cli/src/cvat_cli/version.py index fe68e791718f..84e6495dd8d9 100644 --- a/cvat-cli/src/cvat_cli/version.py +++ b/cvat-cli/src/cvat_cli/version.py @@ -1 +1 @@ -VERSION = "2.4.4" +VERSION = "2.5.0" diff --git a/cvat-core/package.json b/cvat-core/package.json index b35b9122a9f0..d8970e107443 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "9.2.0", + "version": "9.3.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", "scripts": { diff --git a/cvat-core/src/analytics-report.ts b/cvat-core/src/analytics-report.ts new file mode 100644 index 000000000000..8d390046d4ab --- /dev/null +++ b/cvat-core/src/analytics-report.ts @@ -0,0 +1,183 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { ArgumentError } from './exceptions'; + +export interface SerializedDataEntry { + date?: string; + value?: number | Record +} + +export interface SerializedTransformBinaryOp { + left: string; + operator: string; + right: string; +} + +export interface SerializedTransformationEntry { + name: string; + binary?: SerializedTransformBinaryOp; +} + +export interface SerializedAnalyticsEntry { + name?: string; + title?: string; + description?: string; + granularity?: string; + default_view?: string; + data_series?: Record; + transformations?: SerializedTransformationEntry[]; +} + +export interface SerializedAnalyticsReport { + id?: number; + target?: string; + created_date?: string; + statistics?: SerializedAnalyticsEntry[]; +} + +export enum AnalyticsReportTarget { + JOB = 'job', + TASK = 'task', + PROJECT = 'project', +} + +export enum AnalyticsEntryViewType { + HISTOGRAM = 'histogram', + NUMERIC = 'numeric', +} + +export class AnalyticsEntry { + #name: string; + #title: string; + #description: string; + #granularity: string; + #defaultView: AnalyticsEntryViewType; + #dataSeries: Record; + #transformations: SerializedTransformationEntry[]; + + constructor(initialData: SerializedAnalyticsEntry) { + this.#name = initialData.name; + this.#title = initialData.title; + this.#description = initialData.description; + this.#granularity = initialData.granularity; + this.#defaultView = initialData.default_view as AnalyticsEntryViewType; + this.#transformations = initialData.transformations; + this.#dataSeries = this.applyTransformations(initialData.data_series); + } + + get name(): string { + return this.#name; + } + + get title(): string { + return this.#title; + } + + get description(): string { + return this.#description; + } + + // Probably need to create enum for this + get granularity(): string { + return this.#granularity; + } + + get defaultView(): AnalyticsEntryViewType { + return this.#defaultView; + } + + get dataSeries(): Record { + return this.#dataSeries; + } + + get transformations(): SerializedTransformationEntry[] { + return this.#transformations; + } + + private applyTransformations( + dataSeries: Record, + ): Record { + this.#transformations.forEach((transform) => { + if (transform.binary) { + let operator: (left: number, right: number) => number; + switch (transform.binary.operator) { + case '+': { + operator = (left: number, right: number) => left + right; + break; + } + case '-': { + operator = (left: number, right: number) => left - right; + break; + } + case '*': { + operator = (left: number, right: number) => left * right; + break; + } + case '/': { + operator = (left: number, right: number) => (right !== 0 ? left / right : 0); + break; + } + default: { + throw new ArgumentError( + `Cannot apply transformation: got unsupported operator type ${transform.binary.operator}.`, + ); + } + } + + const leftName = transform.binary.left; + const rightName = transform.binary.right; + dataSeries[transform.name] = dataSeries[leftName].map((left, i) => { + const right = dataSeries[rightName][i]; + if (typeof left.value === 'number' && typeof right.value === 'number') { + return { + value: operator(left.value, right.value), + date: left.date, + }; + } + return { + value: 0, + date: left.date, + }; + }); + delete dataSeries[leftName]; + delete dataSeries[rightName]; + } + }); + return dataSeries; + } +} + +export default class AnalyticsReport { + #id: number; + #target: AnalyticsReportTarget; + #createdDate: string; + #statistics: AnalyticsEntry[]; + + constructor(initialData: SerializedAnalyticsReport) { + this.#id = initialData.id; + this.#target = initialData.target as AnalyticsReportTarget; + this.#createdDate = initialData.created_date; + this.#statistics = []; + for (const analyticsEntry of initialData.statistics) { + this.#statistics.push(new AnalyticsEntry(analyticsEntry)); + } + } + + get id(): number { + return this.#id; + } + + get target(): AnalyticsReportTarget { + return this.#target; + } + + get createdDate(): string { + return this.#createdDate; + } + + get statistics(): AnalyticsEntry[] { + return this.#statistics; + } +} diff --git a/cvat-core/src/annotations-collection.ts b/cvat-core/src/annotations-collection.ts index 8755867c4e79..afb9f636aa9a 100644 --- a/cvat-core/src/annotations-collection.ts +++ b/cvat-core/src/annotations-collection.ts @@ -659,18 +659,32 @@ export default class Collection { fillBody(Object.values(this.labels).filter((label) => !label.hasParent)); const scanTrack = (track, prefix = ''): void => { + const countInterpolatedFrames = (start: number, stop: number, lastIsKeyframe: boolean): number => { + let count = stop - start; + if (lastIsKeyframe) { + count -= 1; + } + for (let i = start + 1; lastIsKeyframe ? i < stop : i <= stop; i++) { + if (this.frameMeta.deleted_frames[i]) { + count--; + } + } + return count; + }; + const pref = prefix ? `${prefix}${sep}` : ''; const label = `${pref}${track.label.name}`; labels[label][track.shapeType].track++; const keyframes = Object.keys(track.shapes) .sort((a, b) => +a - +b) - .map((el) => +el); + .map((el) => +el) + .filter((frame) => !this.frameMeta.deleted_frames[frame]); let prevKeyframe = keyframes[0]; let visible = false; for (const keyframe of keyframes) { if (visible) { - const interpolated = keyframe - prevKeyframe - 1; + const interpolated = countInterpolatedFrames(prevKeyframe, keyframe, true); labels[label].interpolated += interpolated; labels[label].total += interpolated; } @@ -692,7 +706,7 @@ export default class Collection { } if (lastKey !== this.stopFrame && !track.get(lastKey).outside) { - const interpolated = this.stopFrame - lastKey; + const interpolated = countInterpolatedFrames(lastKey, this.stopFrame, false); labels[label].interpolated += interpolated; labels[label].total += interpolated; } @@ -719,13 +733,13 @@ export default class Collection { } const { name: label } = object.label; - if (objectType === 'tag') { + if (objectType === 'tag' && !this.frameMeta.deleted_frames[object.frame]) { labels[label].tag++; labels[label].manually++; labels[label].total++; } else if (objectType === 'track') { scanTrack(object); - } else { + } else if (!this.frameMeta.deleted_frames[object.frame]) { const { shapeType } = object as Shape; labels[label][shapeType].shape++; labels[label].manually++; @@ -800,6 +814,7 @@ export default class Collection { frame: state.frame, label_id: state.label.id, group: 0, + source: state.source, }); } else { checkObjectType('state occluded', state.occluded, 'boolean', null); @@ -825,6 +840,7 @@ export default class Collection { frame: state.frame, group: 0, label_id: state.label.id, + outside: state.outside || false, occluded: state.occluded || false, points: state.shapeType === 'mask' ? (() => { const { width, height } = this.frameMeta[state.frame]; @@ -889,7 +905,7 @@ export default class Collection { frame: state.frame, type: element.shapeType, points: [...element.points], - zOrder: state.zOrder, + z_order: state.zOrder, outside: element.outside || false, occluded: element.occluded || false, rotation: element.rotation || 0, diff --git a/cvat-core/src/annotations-objects.ts b/cvat-core/src/annotations-objects.ts index 8b8f3e702448..99212d38bc90 100644 --- a/cvat-core/src/annotations-objects.ts +++ b/cvat-core/src/annotations-objects.ts @@ -1956,6 +1956,7 @@ export class SkeletonShape extends Shape { group: this.group, z_order: this.zOrder, rotation: 0, + elements: undefined, })); const result: RawShapeData = { @@ -2891,7 +2892,24 @@ export class SkeletonTrack extends Track { // Method is used to export data to the server public toJSON(): RawTrackData { const result: RawTrackData = Track.prototype.toJSON.call(this); - result.elements = this.elements.map((el) => el.toJSON()); + result.elements = this.elements.map((el) => ({ + ...el.toJSON(), + elements: undefined, + source: this.source, + group: this.group, + })); + result.elements.forEach((element) => { + element.shapes.forEach((shape) => { + shape.rotation = 0; + const { frame } = shape; + const skeletonShape = result.shapes + .find((_skeletonShape) => _skeletonShape.frame === frame); + if (skeletonShape) { + shape.z_order = skeletonShape.z_order; + } + }); + }); + return result; } diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 6e17a98b4e9d..56e8c1f69701 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -32,6 +32,7 @@ import QualityReport from './quality-report'; import QualityConflict from './quality-conflict'; import QualitySettings from './quality-settings'; import { FramesMetaData } from './frames'; +import AnalyticsReport from './analytics-report'; export default function implementAPI(cvat) { cvat.plugins.list.implementation = PluginRegistry.list; @@ -404,5 +405,38 @@ export default function implementAPI(cvat) { return new FramesMetaData({ ...result }); }; + cvat.analytics.performance.reports.implementation = async (filter) => { + checkFilter(filter, { + jobID: isInteger, + taskID: isInteger, + projectID: isInteger, + startDate: isString, + endDate: isString, + }); + + checkExclusiveFields(filter, ['jobID', 'taskID', 'projectID'], ['startDate', 'endDate']); + + const updatedParams: Record = {}; + + if ('taskID' in filter) { + updatedParams.task_id = filter.taskID; + } + if ('jobID' in filter) { + updatedParams.job_id = filter.jobID; + } + if ('projectID' in filter) { + updatedParams.project_id = filter.projectID; + } + if ('startDate' in filter) { + updatedParams.start_date = filter.startDate; + } + if ('endDate' in filter) { + updatedParams.end_date = filter.endDate; + } + + const reportData = await serverProxy.analytics.performance.reports(updatedParams); + return new AnalyticsReport(reportData); + }; + return cvat; } diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index 72075a2cd97b..4ff47db6ad12 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -279,6 +279,12 @@ function build() { }, }, analytics: { + performance: { + async reports(filter = {}) { + const result = await PluginRegistry.apiWrapper(cvat.analytics.performance.reports, filter); + return result; + }, + }, quality: { async reports(filter: any) { const result = await PluginRegistry.apiWrapper(cvat.analytics.quality.reports, filter); diff --git a/cvat-core/src/enums.ts b/cvat-core/src/enums.ts index 10d381a56e4e..1de28c95237a 100644 --- a/cvat-core/src/enums.ts +++ b/cvat-core/src/enums.ts @@ -86,6 +86,7 @@ export enum Source { MANUAL = 'manual', SEMI_AUTO = 'semi-auto', AUTO = 'auto', + FILE = 'file', } export enum LogType { diff --git a/cvat-core/src/object-state.ts b/cvat-core/src/object-state.ts index 45b652b32b3d..4f7e78ad61c9 100644 --- a/cvat-core/src/object-state.ts +++ b/cvat-core/src/object-state.ts @@ -466,7 +466,7 @@ export default class ObjectState { }), ); - if ([Source.MANUAL, Source.SEMI_AUTO, Source.AUTO].includes(serialized.source)) { + if ([Source.MANUAL, Source.SEMI_AUTO, Source.AUTO, Source.FILE].includes(serialized.source)) { data.source = serialized.source; } if (typeof serialized.zOrder === 'number') { diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 057d1c3ed8ef..6b438d6ca168 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -15,6 +15,7 @@ import { } from 'server-response-types'; import { SerializedQualityReportData } from 'quality-report'; import { SerializedQualitySettingsData } from 'quality-settings'; +import { SerializedAnalyticsReport } from './analytics-report'; import { Storage } from './storage'; import { StorageLocation, WebhookSourceType } from './enums'; import { isEmail, isResourceURL } from './common'; @@ -2317,6 +2318,22 @@ async function getQualityReports(filter): Promise } } +async function getAnalyticsReports(filter): Promise { + const { backendAPI } = config; + + try { + const response = await Axios.get(`${backendAPI}/analytics/reports`, { + params: { + ...filter, + }, + }); + + return response.data; + } catch (errorData) { + throw generateError(errorData); + } +} + export default Object.freeze({ server: Object.freeze({ setAuthData, @@ -2474,6 +2491,9 @@ export default Object.freeze({ }), analytics: Object.freeze({ + performance: Object.freeze({ + reports: getAnalyticsReports, + }), quality: Object.freeze({ reports: getQualityReports, conflicts: getQualityConflicts, diff --git a/cvat-core/tests/api/annotations.js b/cvat-core/tests/api/annotations.js index 3fd6727b78a9..a8bb9ba57154 100644 --- a/cvat-core/tests/api/annotations.js +++ b/cvat-core/tests/api/annotations.js @@ -872,7 +872,29 @@ describe('Feature: get statistics', () => { expect(statistics.label[labelName].manually).toBe(2); expect(statistics.label[labelName].interpolated).toBe(3); expect(statistics.label[labelName].total).toBe(5); + }); + test('get statistics from a job with skeletons', async () => { + const job = (await window.cvat.jobs.get({ jobID: 102 }))[0]; + await job.annotations.clear(true); + let statistics = await job.annotations.statistics(); + expect(statistics.total.manually).toBe(5); + expect(statistics.total.interpolated).toBe(443); + expect(statistics.total.tag).toBe(1); + expect(statistics.total.rectangle.shape).toBe(1); + expect(statistics.total.rectangle.track).toBe(1); + await job.frames.delete(500); // track frame + await job.frames.delete(510); // rectangle shape frame + await job.frames.delete(550); // the first keyframe of a track + statistics = await job.annotations.statistics(); + expect(statistics.total.manually).toBe(2); + expect(statistics.total.tag).toBe(0); + expect(statistics.total.rectangle.shape).toBe(0); + expect(statistics.total.interpolated).toBe(394); + await job.frames.delete(650); // intermediate frame in a track + statistics = await job.annotations.statistics(); + expect(statistics.total.interpolated).toBe(393); + await job.close(); }); }); diff --git a/cvat-sdk/cvat_sdk/core/client.py b/cvat-sdk/cvat_sdk/core/client.py index 1db2de999bd6..9bb61ffc3ed7 100644 --- a/cvat-sdk/cvat_sdk/core/client.py +++ b/cvat-sdk/cvat_sdk/core/client.py @@ -62,8 +62,8 @@ class Client: """ SUPPORTED_SERVER_VERSIONS = ( - pv.Version("2.4"), pv.Version("2.5"), + pv.Version("2.6"), ) def __init__( diff --git a/cvat-sdk/cvat_sdk/datasets/__init__.py b/cvat-sdk/cvat_sdk/datasets/__init__.py new file mode 100644 index 000000000000..08dd89165eac --- /dev/null +++ b/cvat-sdk/cvat_sdk/datasets/__init__.py @@ -0,0 +1,7 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from .caching import UpdatePolicy +from .common import FrameAnnotations, MediaElement, Sample, UnsupportedDatasetError +from .task_dataset import TaskDataset diff --git a/cvat-sdk/cvat_sdk/pytorch/caching.py b/cvat-sdk/cvat_sdk/datasets/caching.py similarity index 100% rename from cvat-sdk/cvat_sdk/pytorch/caching.py rename to cvat-sdk/cvat_sdk/datasets/caching.py diff --git a/cvat-sdk/cvat_sdk/datasets/common.py b/cvat-sdk/cvat_sdk/datasets/common.py new file mode 100644 index 000000000000..2b8269dbd567 --- /dev/null +++ b/cvat-sdk/cvat_sdk/datasets/common.py @@ -0,0 +1,57 @@ +# Copyright (C) 2022-2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +import abc +from typing import List + +import attrs +import attrs.validators +import PIL.Image + +import cvat_sdk.core +import cvat_sdk.core.exceptions +import cvat_sdk.models as models + + +class UnsupportedDatasetError(cvat_sdk.core.exceptions.CvatSdkException): + pass + + +@attrs.frozen +class FrameAnnotations: + """ + Contains annotations that pertain to a single frame. + """ + + tags: List[models.LabeledImage] = attrs.Factory(list) + shapes: List[models.LabeledShape] = attrs.Factory(list) + + +class MediaElement(metaclass=abc.ABCMeta): + """ + The media part of a dataset sample. + """ + + @abc.abstractmethod + def load_image(self) -> PIL.Image.Image: + """ + Loads the media data and returns it as a PIL Image object. + """ + ... + + +@attrs.frozen +class Sample: + """ + Represents an element of a dataset. + """ + + frame_index: int + """Index of the corresponding frame in its task.""" + + annotations: FrameAnnotations + """Annotations belonging to the frame.""" + + media: MediaElement + """Media data of the frame.""" diff --git a/cvat-sdk/cvat_sdk/datasets/task_dataset.py b/cvat-sdk/cvat_sdk/datasets/task_dataset.py new file mode 100644 index 000000000000..586070457934 --- /dev/null +++ b/cvat-sdk/cvat_sdk/datasets/task_dataset.py @@ -0,0 +1,164 @@ +# Copyright (C) 2022-2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import zipfile +from concurrent.futures import ThreadPoolExecutor +from typing import Sequence + +import PIL.Image + +import cvat_sdk.core +import cvat_sdk.core.exceptions +import cvat_sdk.models as models +from cvat_sdk.datasets.caching import UpdatePolicy, make_cache_manager +from cvat_sdk.datasets.common import FrameAnnotations, MediaElement, Sample, UnsupportedDatasetError + +_NUM_DOWNLOAD_THREADS = 4 + + +class TaskDataset: + """ + Represents a task on a CVAT server as a collection of samples. + + Each sample corresponds to one frame in the task, and provides access to + the corresponding annotations and media data. Deleted frames are omitted. + + This class caches all data and annotations for the task on the local file system + during construction. + + Limitations: + + * Only tasks with image (not video) data are supported at the moment. + * Track annotations are currently not accessible. + """ + + class _TaskMediaElement(MediaElement): + def __init__(self, dataset: TaskDataset, frame_index: int) -> None: + self._dataset = dataset + self._frame_index = frame_index + + def load_image(self) -> PIL.Image.Image: + return self._dataset._load_frame_image(self._frame_index) + + def __init__( + self, + client: cvat_sdk.core.Client, + task_id: int, + *, + update_policy: UpdatePolicy = UpdatePolicy.IF_MISSING_OR_STALE, + ) -> None: + """ + Creates a dataset corresponding to the task with ID `task_id` on the + server that `client` is connected to. + + `update_policy` determines when and if the local cache will be updated. + """ + + self._logger = client.logger + + cache_manager = make_cache_manager(client, update_policy) + self._task = cache_manager.retrieve_task(task_id) + + if not self._task.size or not self._task.data_chunk_size: + raise UnsupportedDatasetError("The task has no data") + + if self._task.data_original_chunk_type != "imageset": + raise UnsupportedDatasetError( + f"{self.__class__.__name__} only supports tasks with image chunks;" + f" current chunk type is {self._task.data_original_chunk_type!r}" + ) + + self._logger.info("Fetching labels...") + self._labels = tuple(self._task.get_labels()) + + data_meta = cache_manager.ensure_task_model( + self._task.id, + "data_meta.json", + models.DataMetaRead, + self._task.get_meta, + "data metadata", + ) + + active_frame_indexes = set(range(self._task.size)) - set(data_meta.deleted_frames) + + self._logger.info("Downloading chunks...") + + self._chunk_dir = cache_manager.chunk_dir(task_id) + self._chunk_dir.mkdir(exist_ok=True, parents=True) + + needed_chunks = {index // self._task.data_chunk_size for index in active_frame_indexes} + + with ThreadPoolExecutor(_NUM_DOWNLOAD_THREADS) as pool: + + def ensure_chunk(chunk_index): + cache_manager.ensure_chunk(self._task, chunk_index) + + for _ in pool.map(ensure_chunk, sorted(needed_chunks)): + # just need to loop through all results so that any exceptions are propagated + pass + + self._logger.info("All chunks downloaded") + + annotations = cache_manager.ensure_task_model( + self._task.id, + "annotations.json", + models.LabeledData, + self._task.get_annotations, + "annotations", + ) + + self._frame_annotations = { + frame_index: FrameAnnotations() for frame_index in sorted(active_frame_indexes) + } + + for tag in annotations.tags: + # Some annotations may belong to deleted frames; skip those. + if tag.frame in self._frame_annotations: + self._frame_annotations[tag.frame].tags.append(tag) + + for shape in annotations.shapes: + if shape.frame in self._frame_annotations: + self._frame_annotations[shape.frame].shapes.append(shape) + + # TODO: tracks? + + self._samples = [ + Sample(frame_index=k, annotations=v, media=self._TaskMediaElement(self, k)) + for k, v in self._frame_annotations.items() + ] + + @property + def labels(self) -> Sequence[models.ILabel]: + """ + Returns the labels configured in the task. + + Clients must not modify the object returned by this property or its components. + """ + return self._labels + + @property + def samples(self) -> Sequence[Sample]: + """ + Returns a sequence of all samples, in order of their frame indices. + + Note that the frame indices may not be contiguous, as deleted frames will not be included. + + Clients must not modify the object returned by this property or its components. + """ + return self._samples + + def _load_frame_image(self, frame_index: int) -> PIL.Image: + assert frame_index in self._frame_annotations + + chunk_index = frame_index // self._task.data_chunk_size + member_index = frame_index % self._task.data_chunk_size + + with zipfile.ZipFile(self._chunk_dir / f"{chunk_index}.zip", "r") as chunk_zip: + with chunk_zip.open(chunk_zip.infolist()[member_index]) as chunk_member: + image = PIL.Image.open(chunk_member) + image.load() + + return image diff --git a/cvat-sdk/cvat_sdk/pytorch/__init__.py b/cvat-sdk/cvat_sdk/pytorch/__init__.py index ba6609b268a4..3fa537ff99c0 100644 --- a/cvat-sdk/cvat_sdk/pytorch/__init__.py +++ b/cvat-sdk/cvat_sdk/pytorch/__init__.py @@ -2,8 +2,12 @@ # # SPDX-License-Identifier: MIT -from .caching import UpdatePolicy -from .common import FrameAnnotations, Target, UnsupportedDatasetError +from .common import Target from .project_dataset import ProjectVisionDataset from .task_dataset import TaskVisionDataset from .transforms import ExtractBoundingBoxes, ExtractSingleLabelIndex, LabeledBoxes + +# isort: split +# Compatibility imports +from ..datasets.caching import UpdatePolicy +from ..datasets.common import FrameAnnotations, UnsupportedDatasetError diff --git a/cvat-sdk/cvat_sdk/pytorch/common.py b/cvat-sdk/cvat_sdk/pytorch/common.py index ac5d8fb7ad96..97ef38bc33a8 100644 --- a/cvat-sdk/cvat_sdk/pytorch/common.py +++ b/cvat-sdk/cvat_sdk/pytorch/common.py @@ -2,28 +2,11 @@ # # SPDX-License-Identifier: MIT -from typing import List, Mapping +from typing import Mapping import attrs -import attrs.validators -import cvat_sdk.core -import cvat_sdk.core.exceptions -import cvat_sdk.models as models - - -class UnsupportedDatasetError(cvat_sdk.core.exceptions.CvatSdkException): - pass - - -@attrs.frozen -class FrameAnnotations: - """ - Contains annotations that pertain to a single frame. - """ - - tags: List[models.LabeledImage] = attrs.Factory(list) - shapes: List[models.LabeledShape] = attrs.Factory(list) +from cvat_sdk.datasets.common import FrameAnnotations @attrs.frozen diff --git a/cvat-sdk/cvat_sdk/pytorch/project_dataset.py b/cvat-sdk/cvat_sdk/pytorch/project_dataset.py index be834b1cedd9..ada554ee1210 100644 --- a/cvat-sdk/cvat_sdk/pytorch/project_dataset.py +++ b/cvat-sdk/cvat_sdk/pytorch/project_dataset.py @@ -12,7 +12,7 @@ import cvat_sdk.core import cvat_sdk.core.exceptions import cvat_sdk.models as models -from cvat_sdk.pytorch.caching import UpdatePolicy, make_cache_manager +from cvat_sdk.datasets.caching import UpdatePolicy, make_cache_manager from cvat_sdk.pytorch.task_dataset import TaskVisionDataset diff --git a/cvat-sdk/cvat_sdk/pytorch/task_dataset.py b/cvat-sdk/cvat_sdk/pytorch/task_dataset.py index 6edd3ec24aa2..8964d2db47db 100644 --- a/cvat-sdk/cvat_sdk/pytorch/task_dataset.py +++ b/cvat-sdk/cvat_sdk/pytorch/task_dataset.py @@ -2,21 +2,17 @@ # # SPDX-License-Identifier: MIT -import collections import os import types -import zipfile -from concurrent.futures import ThreadPoolExecutor -from typing import Callable, Dict, Mapping, Optional +from typing import Callable, Mapping, Optional -import PIL.Image import torchvision.datasets import cvat_sdk.core import cvat_sdk.core.exceptions -import cvat_sdk.models as models -from cvat_sdk.pytorch.caching import UpdatePolicy, make_cache_manager -from cvat_sdk.pytorch.common import FrameAnnotations, Target, UnsupportedDatasetError +from cvat_sdk.datasets.caching import UpdatePolicy, make_cache_manager +from cvat_sdk.datasets.task_dataset import TaskDataset +from cvat_sdk.pytorch.common import Target _NUM_DOWNLOAD_THREADS = 4 @@ -75,92 +71,31 @@ def __init__( `update_policy` determines when and if the local cache will be updated. """ - self._logger = client.logger + self._underlying = TaskDataset(client, task_id, update_policy=update_policy) cache_manager = make_cache_manager(client, update_policy) - self._task = cache_manager.retrieve_task(task_id) - - if not self._task.size or not self._task.data_chunk_size: - raise UnsupportedDatasetError("The task has no data") - - if self._task.data_original_chunk_type != "imageset": - raise UnsupportedDatasetError( - f"{self.__class__.__name__} only supports tasks with image chunks;" - f" current chunk type is {self._task.data_original_chunk_type!r}" - ) super().__init__( - os.fspath(cache_manager.task_dir(self._task.id)), + os.fspath(cache_manager.task_dir(task_id)), transforms=transforms, transform=transform, target_transform=target_transform, ) - data_meta = cache_manager.ensure_task_model( - self._task.id, - "data_meta.json", - models.DataMetaRead, - self._task.get_meta, - "data metadata", - ) - self._active_frame_indexes = sorted( - set(range(self._task.size)) - set(data_meta.deleted_frames) - ) - - self._logger.info("Downloading chunks...") - - self._chunk_dir = cache_manager.chunk_dir(task_id) - self._chunk_dir.mkdir(exist_ok=True, parents=True) - - needed_chunks = { - index // self._task.data_chunk_size for index in self._active_frame_indexes - } - - with ThreadPoolExecutor(_NUM_DOWNLOAD_THREADS) as pool: - - def ensure_chunk(chunk_index): - cache_manager.ensure_chunk(self._task, chunk_index) - - for _ in pool.map(ensure_chunk, sorted(needed_chunks)): - # just need to loop through all results so that any exceptions are propagated - pass - - self._logger.info("All chunks downloaded") - if label_name_to_index is None: self._label_id_to_index = types.MappingProxyType( { label.id: label_index for label_index, label in enumerate( - sorted(self._task.get_labels(), key=lambda l: l.id) + sorted(self._underlying.labels, key=lambda l: l.id) ) } ) else: self._label_id_to_index = types.MappingProxyType( - {label.id: label_name_to_index[label.name] for label in self._task.get_labels()} + {label.id: label_name_to_index[label.name] for label in self._underlying.labels} ) - annotations = cache_manager.ensure_task_model( - self._task.id, - "annotations.json", - models.LabeledData, - self._task.get_annotations, - "annotations", - ) - - self._frame_annotations: Dict[int, FrameAnnotations] = collections.defaultdict( - FrameAnnotations - ) - - for tag in annotations.tags: - self._frame_annotations[tag.frame].tags.append(tag) - - for shape in annotations.shapes: - self._frame_annotations[shape.frame].shapes.append(shape) - - # TODO: tracks? - def __getitem__(self, sample_index: int): """ Returns the sample with index `sample_index`. @@ -168,19 +103,10 @@ def __getitem__(self, sample_index: int): `sample_index` must satisfy the condition `0 <= sample_index < len(self)`. """ - frame_index = self._active_frame_indexes[sample_index] - chunk_index = frame_index // self._task.data_chunk_size - member_index = frame_index % self._task.data_chunk_size + sample = self._underlying.samples[sample_index] - with zipfile.ZipFile(self._chunk_dir / f"{chunk_index}.zip", "r") as chunk_zip: - with chunk_zip.open(chunk_zip.infolist()[member_index]) as chunk_member: - sample_image = PIL.Image.open(chunk_member) - sample_image.load() - - sample_target = Target( - annotations=self._frame_annotations[frame_index], - label_id_to_index=self._label_id_to_index, - ) + sample_image = sample.media.load_image() + sample_target = Target(sample.annotations, self._label_id_to_index) if self.transforms: sample_image, sample_target = self.transforms(sample_image, sample_target) @@ -188,4 +114,4 @@ def __getitem__(self, sample_index: int): def __len__(self) -> int: """Returns the number of samples in the dataset.""" - return len(self._active_frame_indexes) + return len(self._underlying.samples) diff --git a/cvat-sdk/cvat_sdk/pytorch/transforms.py b/cvat-sdk/cvat_sdk/pytorch/transforms.py index 259ebc045375..d63fdba65f68 100644 --- a/cvat-sdk/cvat_sdk/pytorch/transforms.py +++ b/cvat-sdk/cvat_sdk/pytorch/transforms.py @@ -10,7 +10,8 @@ import torch.utils.data from typing_extensions import TypedDict -from cvat_sdk.pytorch.common import Target, UnsupportedDatasetError +from cvat_sdk.datasets.common import UnsupportedDatasetError +from cvat_sdk.pytorch.common import Target @attrs.frozen diff --git a/cvat-sdk/gen/generate.sh b/cvat-sdk/gen/generate.sh index 11ebb3533de6..1b3e8bcb7789 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.4.4" +VERSION="2.5.0" LIB_NAME="cvat_sdk" LAYER1_LIB_NAME="${LIB_NAME}/api_client" DST_DIR="$(cd "$(dirname -- "$0")/.." && pwd)" diff --git a/cvat-sdk/gen/templates/openapi-generator/model_utils.mustache b/cvat-sdk/gen/templates/openapi-generator/model_utils.mustache index 09887e414b2e..c9e2b70d77bb 100644 --- a/cvat-sdk/gen/templates/openapi-generator/model_utils.mustache +++ b/cvat-sdk/gen/templates/openapi-generator/model_utils.mustache @@ -1,7 +1,7 @@ {{>partial_header}} from datetime import date, datetime # noqa: F401 -from copy import deepcopy +from copy import copy, deepcopy import inspect import io import os @@ -1310,14 +1310,18 @@ def validate_and_convert_types(input_value, required_types_mixed, path_to_item, if inner_required_types is None: # for this type, there are not more inner variables left to look at return input_value + if isinstance(input_value, list): - if input_value == []: + # avoid storing and changing the input value when the type is mutable collection + output_value = copy(input_value) + + if output_value == []: # allow an empty list - return input_value - for index, inner_value in enumerate(input_value): + return output_value + for index, inner_value in enumerate(output_value): inner_path = list(path_to_item) inner_path.append(index) - input_value[index] = validate_and_convert_types( + output_value[index] = validate_and_convert_types( inner_value, inner_required_types, inner_path, @@ -1326,16 +1330,19 @@ def validate_and_convert_types(input_value, required_types_mixed, path_to_item, configuration=configuration ) elif isinstance(input_value, dict): - if input_value == {}: + # avoid storing and changing the input value when the type is mutable collection + output_value = copy(input_value) + + if output_value == {}: # allow an empty dict - return input_value - for inner_key, inner_val in input_value.items(): + return output_value + for inner_key, inner_val in output_value.items(): inner_path = list(path_to_item) inner_path.append(inner_key) if get_simple_class(inner_key) != str: raise get_type_error(inner_key, inner_path, valid_classes, key_type=True) - input_value[inner_key] = validate_and_convert_types( + output_value[inner_key] = validate_and_convert_types( inner_val, inner_required_types, inner_path, @@ -1343,7 +1350,10 @@ def validate_and_convert_types(input_value, required_types_mixed, path_to_item, _check_type, configuration=configuration ) - return input_value + else: + output_value = input_value + + return output_value def model_to_dict(model_instance, serialize=True): @@ -1382,24 +1392,20 @@ def model_to_dict(model_instance, serialize=True): except KeyError: used_fallback_python_attribute_names.add(attr) if isinstance(value, list): - if not value: - # empty list or None - result[attr] = value - else: - res = [] - for v in value: - if isinstance(v, PRIMITIVE_TYPES) or v is None: - res.append(v) - elif isinstance(v, ModelSimple): - res.append(v.value) - elif isinstance(v, dict): - res.append(dict(map( - extract_item, - v.items() - ))) - else: - res.append(model_to_dict(v, serialize=serialize)) - result[attr] = res + res = [] + for v in value: + if isinstance(v, PRIMITIVE_TYPES) or v is None: + res.append(v) + elif isinstance(v, ModelSimple): + res.append(v.value) + elif isinstance(v, dict): + res.append(dict(map( + extract_item, + v.items() + ))) + else: + res.append(model_to_dict(v, serialize=serialize)) + result[attr] = res elif isinstance(value, dict): result[attr] = dict(map( extract_item, diff --git a/cvat-ui/package.json b/cvat-ui/package.json index f96a51c6dc7a..5e9569c05029 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.53.0", + "version": "1.54.1", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { @@ -37,6 +37,7 @@ "@types/resize-observer-browser": "^0.1.6", "@uiw/react-md-editor": "^3.22.0", "antd": "~4.18.9", + "chart.js": "^4.3.0", "copy-to-clipboard": "^3.3.1", "cvat-canvas": "link:./../cvat-canvas", "cvat-canvas3d": "link:./../cvat-canvas3d", @@ -53,6 +54,7 @@ "prop-types": "^15.7.2", "react": "^16.14.0", "react-awesome-query-builder": "^4.5.1", + "react-chartjs-2": "^5.2.0", "react-color": "^2.19.3", "react-cookie": "^4.0.3", "react-dom": "^16.14.0", diff --git a/cvat-ui/plugins/sam_plugin/src/ts/index.tsx b/cvat-ui/plugins/sam_plugin/src/ts/index.tsx index 5c9059ba6a86..fe030c035652 100644 --- a/cvat-ui/plugins/sam_plugin/src/ts/index.tsx +++ b/cvat-ui/plugins/sam_plugin/src/ts/index.tsx @@ -27,10 +27,19 @@ interface SAMPlugin { ) => Promise; }; }; + jobs: { + get: { + leave: ( + plugin: SAMPlugin, + results: any[], + query: { jobID?: number } + ) => Promise; + }; + }; }; data: { core: any; - task: any; + jobs: Record; modelID: string; modelURL: string; embeddings: LRUCache; @@ -122,9 +131,23 @@ function modelData( } const samPlugin: SAMPlugin = { - name: 'Segmeny Anything', + name: 'Segment Anything', description: 'Plugin handles non-default SAM serverless function output', cvat: { + jobs: { + get: { + async leave( + plugin: SAMPlugin, + results: any[], + query: { jobID?: number }, + ): Promise { + if (typeof query.jobID === 'number') { + [plugin.data.jobs[query.jobID]] = results; + } + return results; + }, + }, + }, lambda: { call: { async enter( @@ -159,15 +182,19 @@ const samPlugin: SAMPlugin = { mask: number[][]; bounds: [number, number, number, number]; }> { - if (!plugin.data.task || plugin.data.task.id !== taskID) { - [plugin.data.task] = await plugin.data.core.tasks.get({ id: taskID }); - } - const { height: imHeight, width: imWidth } = await plugin.data.task.frames.get(frame); - const key = `${taskID}_${frame}`; if (model.id !== plugin.data.modelID) { return result; } + const job = Object.values(plugin.data.jobs) + .find((_job) => _job.taskId === taskID); + if (!job) { + throw new Error('Could not find a job corresponding to the request'); + } + + const { height: imHeight, width: imWidth } = await job.frames.get(frame); + const key = `${taskID}_${frame}`; + if (result) { const bin = window.atob(result.blob); const uint8Array = new Uint8Array(bin.length); @@ -238,7 +265,7 @@ const samPlugin: SAMPlugin = { }, data: { core: null, - task: null, + jobs: {}, modelID: 'pth-facebookresearch-sam-vit-h', modelURL: '/api/lambda/sam_detector.onnx', embeddings: new LRUCache({ @@ -262,9 +289,9 @@ const samPlugin: SAMPlugin = { const SAMModelPlugin: ComponentBuilder = ({ core }) => { samPlugin.data.core = core; + core.plugins.register(samPlugin); InferenceSession.create(samPlugin.data.modelURL).then((session) => { samPlugin.data.session = session; - core.plugins.register(samPlugin); }); return { diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index ebfe3a123a29..8b7a899b9ec3 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -13,7 +13,7 @@ import { RectDrawingMethod, CuboidDrawingMethod, Canvas, CanvasMode as Canvas2DMode, } from 'cvat-canvas-wrapper'; import { - getCore, MLModel, DimensionType, JobType, Job, QualityConflict, + getCore, MLModel, JobType, Job, QualityConflict, } from 'cvat-core-wrapper'; import logger, { LogType } from 'cvat-logger'; import { getCVATStore } from 'cvat-store'; @@ -942,10 +942,11 @@ export function getJobAsync( if (report) conflicts = await cvat.analytics.quality.conflicts({ reportId: report.id }); } - // navigate to correct first frame according to setup - const frameNumber = (await job.frames.search( - { notDeleted: !showDeletedFrames }, job.startFrame, job.stopFrame, - )) || job.startFrame; + // frame query parameter does not work for GT job + const frameNumber = Number.isInteger(initialFrame) && groundTruthJobId !== job.id ? + initialFrame : (await job.frames.search( + { notDeleted: !showDeletedFrames }, job.startFrame, job.stopFrame, + )) || job.startFrame; const frameData = await job.frames.get(frameNumber); // call first getting of frame data before rendering interface @@ -993,11 +994,6 @@ export function getJobAsync( }, }); - if (job.dimension === DimensionType.DIMENSION_3D) { - const workspace = Workspace.STANDARD3D; - dispatch(changeWorkspace(workspace)); - } - dispatch(changeFrameAsync(frameNumber, false)); } catch (error) { dispatch({ diff --git a/cvat-ui/src/components/analytics-page/analytics-page.tsx b/cvat-ui/src/components/analytics-page/analytics-page.tsx new file mode 100644 index 000000000000..bb0a54d98888 --- /dev/null +++ b/cvat-ui/src/components/analytics-page/analytics-page.tsx @@ -0,0 +1,364 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useCallback, useEffect, useState } from 'react'; +import { useLocation, useParams } from 'react-router'; +import { Link } from 'react-router-dom'; +import { Row, Col } from 'antd/lib/grid'; +import Tabs from 'antd/lib/tabs'; +import Text from 'antd/lib/typography/Text'; +import Title from 'antd/lib/typography/Title'; +import notification from 'antd/lib/notification'; +import { useIsMounted } from 'utils/hooks'; +import { Project, Task } from 'reducers'; +import { AnalyticsReport, Job, getCore } from 'cvat-core-wrapper'; +import moment from 'moment'; +import CVATLoadingSpinner from 'components/common/loading-spinner'; +import GoBackButton from 'components/common/go-back-button'; +import AnalyticsOverview, { DateIntervals } from './analytics-performance'; +import TaskQualityComponent from './quality/task-quality-component'; + +const core = getCore(); + +function handleTimePeriod(interval: DateIntervals): [string, string] { + const now = moment.utc(); + switch (interval) { + case DateIntervals.LAST_WEEK: { + return [now.format(), now.subtract(7, 'd').format()]; + } + case DateIntervals.LAST_MONTH: { + return [now.format(), now.subtract(30, 'd').format()]; + } + case DateIntervals.LAST_QUARTER: { + return [now.format(), now.subtract(90, 'd').format()]; + } + case DateIntervals.LAST_YEAR: { + return [now.format(), now.subtract(365, 'd').format()]; + } + default: { + throw Error(`Date interval is not supported: ${interval}`); + } + } +} + +function AnalyticsPage(): JSX.Element { + const location = useLocation(); + let instanceType = ''; + if (location.pathname.includes('projects')) { + instanceType = 'project'; + } else if (location.pathname.includes('jobs')) { + instanceType = 'job'; + } else { + instanceType = 'task'; + } + + const [fetching, setFetching] = useState(true); + const [instance, setInstance] = useState(null); + const [analyticsReportInstance, setAnalyticsReportInstance] = useState(null); + const isMounted = useIsMounted(); + + let instanceID: number | null = null; + let reportRequestID: number | null = null; + switch (instanceType) { + case 'project': { + instanceID = +useParams<{ pid: string }>().pid; + reportRequestID = +useParams<{ pid: string }>().pid; + break; + } + case 'task': { + instanceID = +useParams<{ tid: string }>().tid; + reportRequestID = +useParams<{ tid: string }>().tid; + break; + } + case 'job': { + instanceID = +useParams<{ jid: string }>().jid; + reportRequestID = +useParams<{ jid: string }>().jid; + break; + } + default: { + throw new Error(`Unsupported instance type ${instanceType}`); + } + } + + const receieveInstance = (): void => { + let instanceRequest = null; + switch (instanceType) { + case 'project': { + instanceRequest = core.projects.get({ id: instanceID }); + break; + } + case 'task': { + instanceRequest = core.tasks.get({ id: instanceID }); + break; + } + case 'job': + { + instanceRequest = core.jobs.get({ jobID: instanceID }); + break; + } + default: { + throw new Error(`Unsupported instance type ${instanceType}`); + } + } + + if (Number.isInteger(instanceID)) { + instanceRequest + .then(([_instance]: Task[] | Project[] | Job[]) => { + if (isMounted() && _instance) { + setInstance(_instance); + } + }).catch((error: Error) => { + if (isMounted()) { + notification.error({ + message: 'Could not receive the requested instance from the server', + description: error.toString(), + }); + } + }).finally(() => { + if (isMounted()) { + setFetching(false); + } + }); + } else { + notification.error({ + message: 'Could not receive the requested task from the server', + description: `Requested "${instanceID}" is not valid`, + }); + setFetching(false); + } + }; + + const receieveReport = (timeInterval: DateIntervals): void => { + if (Number.isInteger(instanceID) && Number.isInteger(reportRequestID)) { + let reportRequest = null; + const [endDate, startDate] = handleTimePeriod(timeInterval); + + switch (instanceType) { + case 'project': { + reportRequest = core.analytics.performance.reports({ + projectID: reportRequestID, + endDate, + startDate, + }); + break; + } + case 'task': { + reportRequest = core.analytics.performance.reports({ + taskID: reportRequestID, + endDate, + startDate, + }); + break; + } + case 'job': { + reportRequest = core.analytics.performance.reports({ + jobID: reportRequestID, + endDate, + startDate, + }); + break; + } + default: { + throw new Error(`Unsupported instance type ${instanceType}`); + } + } + + reportRequest + .then((report: AnalyticsReport) => { + if (isMounted() && report) { + setAnalyticsReportInstance(report); + } + }).catch((error: Error) => { + if (isMounted()) { + notification.error({ + message: 'Could not receive the requested report from the server', + description: error.toString(), + }); + } + }); + } + }; + + useEffect((): void => { + Promise.all([receieveInstance(), receieveReport(DateIntervals.LAST_WEEK)]).finally(() => { + if (isMounted()) { + setFetching(false); + } + }); + }, []); + + const onJobUpdate = useCallback((job: Job): void => { + setFetching(true); + job.save().then(() => { + if (isMounted()) { + receieveInstance(); + } + }).catch((error: Error) => { + if (isMounted()) { + notification.error({ + message: 'Could not update the job', + description: error.toString(), + }); + } + }).finally(() => { + if (isMounted()) { + setFetching(false); + } + }); + }, []); + + const onAnalyticsTimePeriodChange = useCallback((val: DateIntervals): void => { + receieveReport(val); + }, []); + + let backNavigation: JSX.Element | null = null; + let title: JSX.Element | null = null; + let tabs: JSX.Element | null = null; + if (instance) { + switch (instanceType) { + case 'project': { + backNavigation = ( + + + + ); + title = ( + + + Analytics for + {' '} + <Link to={`/projects/${instance.id}`}>{`Project #${instance.id}`}</Link> + + + ); + tabs = ( + + + Performance + + )} + key='Overview' + > + + + + ); + break; + } + case 'task': { + backNavigation = ( + + + + ); + title = ( + + + Analytics for + {' '} + <Link to={`/tasks/${instance.id}`}>{`Task #${instance.id}`}</Link> + + + ); + tabs = ( + + + Performance + + )} + key='overview' + > + + + + Quality + + )} + key='quality' + > + + + + ); + break; + } + case 'job': + { + backNavigation = ( + + + + ); + title = ( + + + Analytics for + {' '} + <Link to={`/tasks/${instance.taskId}/jobs/${instance.id}`}>{`Job #${instance.id}`}</Link> + + + ); + tabs = ( + + + Performance + + )} + key='overview' + > + + + + ); + break; + } + default: { + throw new Error(`Unsupported instance type ${instanceType}`); + } + } + } + + return ( +
+ { + fetching ? ( +
+ +
+ ) : ( + + {backNavigation} + + {title} + {tabs} + + + ) + } +
+ ); +} + +export default React.memo(AnalyticsPage); diff --git a/cvat-ui/src/components/analytics-page/analytics-performance.tsx b/cvat-ui/src/components/analytics-page/analytics-performance.tsx new file mode 100644 index 000000000000..061486039cfb --- /dev/null +++ b/cvat-ui/src/components/analytics-page/analytics-performance.tsx @@ -0,0 +1,194 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; + +import React from 'react'; +import moment from 'moment'; +import RGL, { WidthProvider } from 'react-grid-layout'; +import Text from 'antd/lib/typography/Text'; +import Select from 'antd/lib/select'; +import Notification from 'antd/lib/notification'; +import { AnalyticsReport, AnalyticsEntryViewType } from 'cvat-core-wrapper'; +import { Col, Row } from 'antd/lib/grid'; +import HistogramView from './views/histogram-view'; +import AnalyticsCard from './views/analytics-card'; + +const ReactGridLayout = WidthProvider(RGL); + +export enum DateIntervals { + LAST_WEEK = 'Last 7 days', + LAST_MONTH = 'Last 30 days', + LAST_QUARTER = 'Last 90 days', + LAST_YEAR = 'Last year', +} + +interface Props { + report: AnalyticsReport | null; + onTimePeriodChange: (val: DateIntervals) => void; +} + +const colors = [ + 'rgba(255, 99, 132, 0.5)', + 'rgba(53, 162, 235, 0.5)', + 'rgba(170, 83, 85, 0.5)', + 'rgba(44, 70, 94, 0.5)', + 'rgba(28, 66, 98, 0.5)', +]; + +function AnalyticsOverview(props: Props): JSX.Element | null { + const { report, onTimePeriodChange } = props; + + if (!report) return null; + const layout: any = []; + let histogramCount = 0; + let numericCount = 0; + const views: any = []; + report.statistics.forEach((entry) => { + const tooltip = ( +
+ + {entry.description} + +
+ ); + switch (entry.defaultView) { + case AnalyticsEntryViewType.NUMERIC: { + layout.push({ + i: entry.name, + w: 2, + h: 1, + x: 2, + y: numericCount, + }); + numericCount += 1; + const { value } = entry.dataSeries[Object.keys(entry.dataSeries)[0]][0]; + + views.push({ + view: ( + + ), + key: entry.name, + }); + break; + } + case AnalyticsEntryViewType.HISTOGRAM: { + const firstDataset = Object.keys(entry.dataSeries)[0]; + const dateLabels = entry.dataSeries[firstDataset].map((dataEntry) => ( + moment.utc(dataEntry.date).local().format('YYYY-MM-DD') + )); + + const { dataSeries } = entry; + let colorIndex = -1; + const datasets = Object.entries(dataSeries).map(([key, series]) => { + let label = key.split('_').join(' '); + label = label.charAt(0).toUpperCase() + label.slice(1); + + const data: number[] = series.map((s) => { + if (typeof s.value === 'number') { + return s.value as number; + } + + if (typeof s.value === 'object') { + return Object.keys(s.value).reduce((acc, k) => acc + s.value[k], 0); + } + + return 0; + }); + + colorIndex = colorIndex >= colors.length - 1 ? 0 : colorIndex + 1; + return { + label, + data, + backgroundColor: colors[colorIndex], + }; + }); + layout.push({ + i: entry.name, + h: 1, + w: 2, + x: 0, + y: histogramCount, + }); + histogramCount += 1; + views.push({ + view: ( + + ), + key: entry.name, + }); + break; + } + default: { + Notification.warning({ + message: `Cannot display analytics view with view type ${entry.defaultView}`, + }); + } + } + }); + return ( +
+ + + + Created + {report?.createdDate ? moment(report?.createdDate).fromNow() : ''} + + + + ) => { + value={currentValue} + onChange={(event: React.ChangeEvent) => { const { value } = event.target; - if (inputType === 'number') { - if (value !== '') { - const numberValue = +value; - if (!Number.isNaN(numberValue)) { - onChange(`${numberValue}`); - } - } - } else { - onChange(value); + if (ref.current?.resizableTextArea?.textArea) { + setSelectionStart(ref.current.resizableTextArea.textArea.selectionStart); } + onChange(value); }} onKeyDown={handleKeydown} /> @@ -119,6 +152,8 @@ function renderInputElement(parameters: InputElementParameters): JSX.Element { element = renderSelect(); } else if (inputType === 'radio') { element = renderRadio(); + } else if (inputType === 'number') { + element = renderNumber(); } else { element = renderText(); } diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-context-menu.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-context-menu.tsx index 88b4fa67f5d9..89b57cc54fd7 100644 --- a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-context-menu.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-context-menu.tsx @@ -128,7 +128,7 @@ export default function CanvasContextMenu(props: Props): JSX.Element | null { (annotationConflict: AnnotationConflict) => annotationConflict.clientID === state.clientID, )); - const copyObject = state.isGroundTruth ? state : null; + const copyObject = state?.isGroundTruth ? state : null; if (workspace === Workspace.REVIEW_WORKSPACE) { return ReactDOM.createPortal( diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-attribute.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-attribute.tsx index ca930a0fc818..53f28619fb4e 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-attribute.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-attribute.tsx @@ -7,12 +7,12 @@ import { Col } from 'antd/lib/grid'; import Select from 'antd/lib/select'; import Radio, { RadioChangeEvent } from 'antd/lib/radio'; import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox'; -import Input from 'antd/lib/input'; import InputNumber from 'antd/lib/input-number'; import Text from 'antd/lib/typography/Text'; import config from 'config'; import { clamp } from 'utils/math'; +import TextArea, { TextAreaRef } from 'antd/lib/input/TextArea'; interface Props { readonly: boolean; @@ -39,10 +39,21 @@ function attrIsTheSame(prevProps: Props, nextProps: Props): boolean { function ItemAttributeComponent(props: Props): JSX.Element { const { - attrInputType, attrValues, attrValue, attrName, attrID, readonly, changeAttribute, + attrInputType, attrValues, attrValue, + attrName, attrID, readonly, changeAttribute, } = props; const attrNameStyle: React.CSSProperties = { wordBreak: 'break-word', lineHeight: '1em', fontSize: 12 }; + const ref = useRef(null); + const [selectionStart, setSelectionStart] = useState(attrValue.length); + + useEffect(() => { + const textArea = ref?.current?.resizableTextArea?.textArea; + if (textArea instanceof HTMLTextAreaElement) { + textArea.selectionStart = selectionStart; + textArea.selectionEnd = selectionStart; + } + }, [attrValue]); if (attrInputType === 'checkbox') { return ( @@ -150,44 +161,24 @@ function ItemAttributeComponent(props: Props): JSX.Element { ); } - const ref = useRef(null); - const [selection, setSelection] = useState<{ - start: number | null; - end: number | null; - direction: 'forward' | 'backward' | 'none' | null; - }>({ - start: null, - end: null, - direction: null, - }); - - useEffect(() => { - if (ref.current && ref.current.input) { - ref.current.input.selectionStart = selection.start; - ref.current.input.selectionEnd = selection.end; - ref.current.input.selectionDirection = selection.direction; - } - }, [attrValue]); - return ( <> {attrName} - ): void => { - if (ref.current && ref.current.input) { - setSelection({ - start: ref.current.input.selectionStart, - end: ref.current.input.selectionEnd, - direction: ref.current.input.selectionDirection, - }); + style={{ + height: Math.min(120, attrValue.split('\n').length * 24), + minHeight: Math.min(120, attrValue.split('\n').length * 24), + }} + onChange={(event: React.ChangeEvent): void => { + if (ref.current?.resizableTextArea?.textArea) { + setSelectionStart(ref.current.resizableTextArea.textArea.selectionStart); } - changeAttribute(attrID, event.target.value); }} value={attrValue} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index a75fd7368779..ced24f3c053b 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -10,7 +10,6 @@ import Collapse from 'antd/lib/collapse'; import ObjectButtonsContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-buttons'; import ItemDetailsContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item-details'; import { ObjectType, ShapeType, ColorBy } from 'reducers'; -import { ObjectState } from 'cvat-core-wrapper'; import ObjectItemElementComponent from './object-item-element'; import ItemBasics from './object-item-basics'; @@ -25,7 +24,7 @@ interface Props { labelID: number; isGroundTruth: boolean; locked: boolean; - elements: any[]; + elements: number[]; color: string; colorBy: ColorBy; labels: any[]; @@ -141,29 +140,27 @@ function ObjectItemComponent(props: Props): JSX.Element { /> )} {!!elements.length && ( - <> - - - PARTS -
- - )} - key='elements' - > - {elements.map((element: ObjectState) => ( - - ))} -
-
- + + + PARTS +
+ + )} + key='elements' + > + {elements.map((element: number) => ( + + ))} +
+
)}
diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index d7821f404190..b2a7938ddc18 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -76,8 +76,8 @@ import EmailVerificationSentPage from './email-confirmation-pages/email-verifica import IncorrectEmailConfirmationPage from './email-confirmation-pages/incorrect-email-confirmation'; import CreateModelPage from './create-model-page/create-model-page'; import CreateJobPage from './create-job-page/create-job-page'; -import TaskAnalyticsPage from './task-analytics-page/task-analytics-page'; import OrganizationWatcher from './watchers/organization-watcher'; +import AnalyticsPage from './analytics-page/analytics-page'; interface CVATAppProps { loadFormats: () => void; @@ -463,13 +463,15 @@ class CVATApplication extends React.PureComponent + - + + Go to the bug tracker Import annotations Export annotations + View analytics {[JobStage.ANNOTATION, JobStage.VALIDATION].includes(job.stage) ? Finish the job : null} {job.stage === JobStage.ACCEPTANCE ? diff --git a/cvat-ui/src/components/job-item/job-item.tsx b/cvat-ui/src/components/job-item/job-item.tsx index c97886460e5b..d9a80e5cff1f 100644 --- a/cvat-ui/src/components/job-item/job-item.tsx +++ b/cvat-ui/src/components/job-item/job-item.tsx @@ -133,13 +133,13 @@ function JobItem(props: Props): JSX.Element { Created on - {`${created.format('MMMM Do YYYY HH:MM')}`} + {`${created.format('MMMM Do YYYY HH:mm')}`} Last updated - {`${updated.format('MMMM Do YYYY HH:MM')}`} + {`${updated.format('MMMM Do YYYY HH:mm')}`} diff --git a/cvat-ui/src/components/jobs-page/job-card.tsx b/cvat-ui/src/components/jobs-page/job-card.tsx index 926c5322ad94..112662aa6706 100644 --- a/cvat-ui/src/components/jobs-page/job-card.tsx +++ b/cvat-ui/src/components/jobs-page/job-card.tsx @@ -17,6 +17,7 @@ const useCardHeight = useCardHeightHOC({ containerClassName: 'cvat-jobs-page', siblingClassNames: ['cvat-jobs-page-pagination', 'cvat-jobs-page-top-bar'], paddings: 40, + minHeight: 200, numberOfRows: 3, }); diff --git a/cvat-ui/src/components/jobs-page/styles.scss b/cvat-ui/src/components/jobs-page/styles.scss index fb3c3fae1ffc..1c6c2a1b26b9 100644 --- a/cvat-ui/src/components/jobs-page/styles.scss +++ b/cvat-ui/src/components/jobs-page/styles.scss @@ -3,13 +3,14 @@ // // SPDX-License-Identifier: MIT -@import '../../base.scss'; +@import '../../base'; .cvat-jobs-page { padding-top: $grid-unit-size * 2; padding-bottom: $grid-unit-size; height: 100%; width: 100%; + overflow: auto; > div:nth-child(1) { div > { diff --git a/cvat-ui/src/components/labels-editor/label-form.tsx b/cvat-ui/src/components/labels-editor/label-form.tsx index 7bf295af5645..600dc11af72b 100644 --- a/cvat-ui/src/components/labels-editor/label-form.tsx +++ b/cvat-ui/src/components/labels-editor/label-form.tsx @@ -10,8 +10,10 @@ import Input from 'antd/lib/input'; import Button from 'antd/lib/button'; import Checkbox from 'antd/lib/checkbox'; import Select from 'antd/lib/select'; +import Tag from 'antd/lib/tag'; import Form, { FormInstance } from 'antd/lib/form'; import Badge from 'antd/lib/badge'; +import Modal from 'antd/lib/modal'; import { Store } from 'antd/lib/form/interface'; import { SerializedAttribute, LabelType } from 'cvat-core-wrapper'; @@ -94,6 +96,8 @@ export default class LabelForm extends React.Component { return { ...attribute, values: attrValues, + default_value: attribute.default_value && attrValues.includes(attribute.default_value) ? + attribute.default_value : attrValues[0], input_type: attribute.type.toLowerCase(), }; }), @@ -117,7 +121,18 @@ export default class LabelForm extends React.Component { private addAttribute = (): void => { if (this.formRef.current) { const attributes = this.formRef.current.getFieldValue('attributes'); - this.formRef.current.setFieldsValue({ attributes: [...(attributes || []), { id: idGenerator() }] }); + this.formRef.current.setFieldsValue({ + attributes: [ + ...(attributes || []), + { + id: idGenerator(), + type: AttributeType.SELECT, + name: '', + values: [], + mutable: false, + }, + ], + }); } }; @@ -131,17 +146,15 @@ export default class LabelForm extends React.Component { }; /* eslint-disable class-methods-use-this */ - private renderAttributeNameInput(fieldInstance: any, attr: SerializedAttribute | null): JSX.Element { + private renderAttributeNameInput(fieldInstance: any, attr: any): JSX.Element { const { key } = fieldInstance; - const locked = attr ? attr.id as number >= 0 : false; - const value = attr ? attr.name : ''; + const attrNames = this.formRef.current?.getFieldValue('attributes') + .filter((_attr: any) => _attr.id !== attr.id).map((_attr: any) => _attr.name); return ( { pattern: patterns.validateAttributeName.pattern, message: patterns.validateAttributeName.message, }, + { + validator: (_rule: any, attrName: string) => { + if (attrNames.includes(attrName) && attr.name !== attrName) { + return Promise.reject(new Error('Attribute name must be unique for the label')); + } + return Promise.resolve(); + }, + }, ]} > - + = 0} className='cvat-attribute-name-input' placeholder='Name' /> ); } - private renderAttributeTypeInput(fieldInstance: any, attr: SerializedAttribute | null): JSX.Element { + private renderAttributeTypeInput(fieldInstance: any, attr: any): JSX.Element { const { key } = fieldInstance; - const locked = attr ? attr.id as number >= 0 : false; - const type = attr ? attr.input_type.toUpperCase() : AttributeType.SELECT; + const locked = attr.id as number >= 0; return ( - - { + const attrs = this.formRef.current?.getFieldValue('attributes'); + if (value === AttributeType.CHECKBOX) { + attrs[key].values = ['false']; + } else if (value === AttributeType.TEXT && !attrs[key].values.length) { + attrs[key].values = ''; + } else if (value === AttributeType.NUMBER || attr.type === AttributeType.CHECKBOX) { + attrs[key].values = []; + } + this.formRef.current?.setFieldsValue({ + attributes: attrs, + }); + }} + > Select @@ -188,10 +224,10 @@ export default class LabelForm extends React.Component { ); } - private renderAttributeValuesInput(fieldInstance: any, attr: SerializedAttribute | null): JSX.Element { + private renderAttributeValuesInput(fieldInstance: any, attr: any): JSX.Element { const { key } = fieldInstance; - const locked = attr ? attr.id as number >= 0 : false; - const existingValues = attr ? attr.values : []; + const locked = attr.id as number >= 0; + const existingValues = attr.values; const validator = (_: any, values: string[]): Promise => { if (locked && existingValues) { @@ -213,8 +249,6 @@ export default class LabelForm extends React.Component { { mode='tags' placeholder='Attribute values' dropdownStyle={{ display: 'none' }} + tagRender={(props) => { + const attrs = this.formRef.current?.getFieldValue('attributes'); + const isDefault = props.value === attrs[key].default_value; + return ( + + { + const parent = window.document.getElementsByClassName('cvat-attribute-values-input')[0]; + if (parent) { + parent.dispatchEvent(new MouseEvent('mouseout', { bubbles: true })); + } + }} + color={isDefault ? 'blue' : undefined} + onClose={() => { + if (isDefault) { + attrs[key].default_value = undefined; + } + props.onClose(); + }} + onClick={() => { + attrs[key].default_value = props.value; + this.formRef.current?.setFieldsValue({ + attributes: attrs, + }); + }} + closable={props.closable} + > + {props.label} + + + ); + }} /> @@ -248,7 +318,6 @@ export default class LabelForm extends React.Component { message: 'Please, specify a default value', }]} name={[key, 'values']} - fieldKey={[fieldInstance.fieldKey, 'values']} > + + ); } - private renderMutableAttributeInput(fieldInstance: any, attr: SerializedAttribute | null): JSX.Element { + private renderMutableAttributeInput(fieldInstance: any, attr: any): JSX.Element { const { key } = fieldInstance; - const locked = attr ? attr.id as number >= 0 : false; - const value = attr ? attr.mutable : false; + const locked = attr.id as number >= 0; return ( @@ -345,19 +409,31 @@ export default class LabelForm extends React.Component { ); } - private renderDeleteAttributeButton(fieldInstance: any, attr: SerializedAttribute | null): JSX.Element { + private renderDeleteAttributeButton(fieldInstance: any, attr: any): JSX.Element { const { key } = fieldInstance; - const locked = attr ? attr.id as number >= 0 : false; return ( - - - - - {taskInstance.name} - - - {`#${taskInstance.id}`} - - - - - Quality - - )} - key='quality' - > - - - - - - ) - } - - ); -} - -export default React.memo(TaskAnalyticsPage); diff --git a/cvat-ui/src/components/tasks-page/styles.scss b/cvat-ui/src/components/tasks-page/styles.scss index 79f5a8fbb8d1..6451d7b3bcf2 100644 --- a/cvat-ui/src/components/tasks-page/styles.scss +++ b/cvat-ui/src/components/tasks-page/styles.scss @@ -3,8 +3,8 @@ // // SPDX-License-Identifier: MIT -@import '../../base.scss'; -@import '../../styles.scss'; +@import '../../base'; +@import '../../styles'; .cvat-tasks-page { padding-top: $grid-unit-size * 2; @@ -44,13 +44,13 @@ padding-top: 10px; } - @media screen and (min-height: 900px) { + @media screen and (height >= 900px) { > div:nth-child(2) { height: 88%; } } - @media screen and (min-height: 1200px) { + @media screen and (height >= 1200px) { > div:nth-child(2) { height: 93%; } diff --git a/cvat-ui/src/containers/annotation-page/canvas/canvas-context-menu.tsx b/cvat-ui/src/containers/annotation-page/canvas/canvas-context-menu.tsx index 2344c1025cf2..74ac036c6270 100644 --- a/cvat-ui/src/containers/annotation-page/canvas/canvas-context-menu.tsx +++ b/cvat-ui/src/containers/annotation-page/canvas/canvas-context-menu.tsx @@ -26,7 +26,7 @@ interface StateToProps { contextMenuParentID: number | null; contextMenuClientID: number | null; canvasInstance: Canvas | null; - objectStates: any[]; + objectStates: ObjectState[]; frameConflicts: QualityConflict[]; visible: boolean; top: number; @@ -155,7 +155,7 @@ class CanvasContextMenuContainer extends React.PureComponent { }; } - static getDerivedStateFromProps(props: Props, state: State): State | null { + static getDerivedStateFromProps(props: Readonly, state: State): State | null { if (props.left === state.latestLeft && props.top === state.latestTop) { return null; } diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx index ac2cba8493be..56bdff93be13 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx @@ -81,7 +81,7 @@ class LabelItemContainer extends React.PureComponent { }; } - static getDerivedStateFromProps(props: Props, state: State): State | null { + static getDerivedStateFromProps(props: Readonly, state: State): State | null { if (props.objectStates === state.objectStates) { return null; } diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index ad9d3491e966..be9817ee6b68 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -23,6 +23,7 @@ import { import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item'; import { getColor } from 'components/annotation-page/standard-workspace/objects-side-bar/shared'; import { shift } from 'utils/math'; +import { Label, ObjectState } from 'cvat-core-wrapper'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; import { Canvas3d } from 'cvat-canvas3d-wrapper'; import { filterApplicableLabels } from 'utils/filter-applicable-labels'; @@ -128,7 +129,34 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { } type Props = StateToProps & DispatchToProps & OwnProps; -class ObjectItemContainer extends React.PureComponent { +interface State { + labels: Label[]; + elements: number[]; +} + +class ObjectItemContainer extends React.PureComponent { + public constructor(props: Props) { + super(props); + this.state = { + labels: props.labels, + elements: props.objectState.elements.map((el: ObjectState) => el.clientID), + }; + } + + public static getDerivedStateFromProps(props: Readonly, state: Readonly): State | null { + const { objectState, labels } = props; + const applicableLabels = filterApplicableLabels(objectState, labels); + if (state.labels.length !== applicableLabels.length || + state.labels.some((label, idx) => label.id !== applicableLabels[idx].id)) { + return { + ...state, + labels: applicableLabels, + }; + } + + return null; + } + private copy = (): void => { const { objectState, readonly, copyShape } = this.props; if (!readonly) { @@ -312,9 +340,9 @@ class ObjectItemContainer extends React.PureComponent { } public render(): JSX.Element { + const { labels, elements } = this.state; const { objectState, - labels, attributes, activated, colorBy, @@ -323,8 +351,6 @@ class ObjectItemContainer extends React.PureComponent { jobInstance, } = this.props; - const applicableLabels = filterApplicableLabels(objectState, labels); - return ( { isGroundTruth={objectState.isGroundTruth} color={getColor(objectState, colorBy)} attributes={attributes} - elements={objectState.elements} + elements={elements} normalizedKeyMap={normalizedKeyMap} - labels={applicableLabels} + labels={labels} colorBy={colorBy} activate={this.activate} remove={this.remove} @@ -354,7 +380,7 @@ class ObjectItemContainer extends React.PureComponent { changeColor={this.changeColor} changeLabel={this.changeLabel} edit={this.edit} - resetCuboidPerspective={() => this.resetCuboidPerspective()} + resetCuboidPerspective={this.resetCuboidPerspective} /> ); } diff --git a/cvat-ui/src/cvat-core-wrapper.ts b/cvat-ui/src/cvat-core-wrapper.ts index 5b7c19bc14c3..888c4413e549 100644 --- a/cvat-ui/src/cvat-core-wrapper.ts +++ b/cvat-ui/src/cvat-core-wrapper.ts @@ -28,6 +28,7 @@ import Comment from 'cvat-core/src/comment'; import User from 'cvat-core/src/user'; import Organization from 'cvat-core/src/organization'; import AnnotationGuide from 'cvat-core/src/guide'; +import AnalyticsReport, { AnalyticsEntryViewType, AnalyticsEntry } from 'cvat-core/src/analytics-report'; import { Dumper } from 'cvat-core/src/annotation-formats'; import { APIWrapperEnterOptions } from 'cvat-core/src/plugins'; @@ -76,6 +77,9 @@ export { AnnotationConflict, ConflictSeverity, FramesMetaData, + AnalyticsReport, + AnalyticsEntry, + AnalyticsEntryViewType, }; export type { diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index a31c6740160e..71524879f8bd 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -221,7 +221,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { instance: job.dimension === DimensionType.DIMENSION_2D ? new Canvas() : new Canvas3d(), }, colors, - workspace: isReview ? Workspace.REVIEW_WORKSPACE : workspaceSelected, + workspace: isReview && job.dimension === DimensionType.DIMENSION_2D ? + Workspace.REVIEW_WORKSPACE : workspaceSelected, }; } case AnnotationActionTypes.GET_JOB_FAILED: { diff --git a/cvat-ui/src/reducers/review-reducer.ts b/cvat-ui/src/reducers/review-reducer.ts index d6032cd5af0e..f261956fba0d 100644 --- a/cvat-ui/src/reducers/review-reducer.ts +++ b/cvat-ui/src/reducers/review-reducer.ts @@ -131,8 +131,24 @@ export default function (state: ReviewState = defaultState, action: any): Review } }); }); - mainConflict.description = descriptionList.join(', '); - mergedFrameConflicts.push(mainConflict); + + // decorate the original conflict to avoid changing it + const description = descriptionList.join(', '); + const visibleConflict = new Proxy(mainConflict, { + get(target, prop) { + if (prop === 'description') { + return description; + } + + // By default, it looks like Reflect.get(target, prop, receiver) + // which has a different value of `this`. It doesn't allow to + // work with methods / properties that use private members. + const val = Reflect.get(target, prop); + return typeof val === 'function' ? (...args: any[]) => val.apply(target, args) : val; + }, + }); + + mergedFrameConflicts.push(visibleConflict); } } } diff --git a/cvat-ui/src/utils/hooks.ts b/cvat-ui/src/utils/hooks.ts index 547955ab5049..0a14853e15ad 100644 --- a/cvat-ui/src/utils/hooks.ts +++ b/cvat-ui/src/utils/hooks.ts @@ -76,6 +76,7 @@ export function useGoBack(): () => void { export interface ICardHeightHOC { numberOfRows: number; + minHeight: number; paddings: number; containerClassName: string; siblingClassNames: string[]; @@ -83,7 +84,7 @@ export interface ICardHeightHOC { export function useCardHeightHOC(params: ICardHeightHOC): () => string { const { - numberOfRows, paddings, containerClassName, siblingClassNames, + numberOfRows, minHeight, paddings, containerClassName, siblingClassNames, } = params; return (): string => { @@ -106,7 +107,7 @@ export function useCardHeightHOC(params: ICardHeightHOC): () => string { }, 0); const cardHeight = (containerHeight - (othersHeight + paddings)) / numberOfRows; - setHeight(`${Math.round(cardHeight)}px`); + setHeight(`${Math.max(Math.round(cardHeight), minHeight)}px`); } }; diff --git a/cvat-ui/webpack.config.js b/cvat-ui/webpack.config.js index 95279a160ff5..f933c2cab7ec 100644 --- a/cvat-ui/webpack.config.js +++ b/cvat-ui/webpack.config.js @@ -128,7 +128,13 @@ module.exports = (env) => { { loader: 'postcss-loader', options: { - plugins: [require('postcss-preset-env')], + postcssOptions: { + plugins: [ + [ + 'postcss-preset-env', {}, + ], + ], + }, }, }, 'sass-loader', diff --git a/cvat/__init__.py b/cvat/__init__.py index e7a3cae5babd..2b4479680b23 100644 --- a/cvat/__init__.py +++ b/cvat/__init__.py @@ -4,6 +4,6 @@ from cvat.utils.version import get_version -VERSION = (2, 5, 1, 'final', 0) +VERSION = (2, 5, 2, 'final', 0) __version__ = get_version(VERSION) diff --git a/cvat/apps/analytics_report/__init__.py b/cvat/apps/analytics_report/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cvat/apps/analytics_report/apps.py b/cvat/apps/analytics_report/apps.py new file mode 100644 index 000000000000..710f95892a0c --- /dev/null +++ b/cvat/apps/analytics_report/apps.py @@ -0,0 +1,20 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from django.apps import AppConfig + + +class EventsConfig(AppConfig): + name = "cvat.apps.analytics_report" + + def ready(self): + from django.conf import settings + + from . import default_settings + + for key in dir(default_settings): + if key.isupper() and not hasattr(settings, key): + setattr(settings, key, getattr(default_settings, key)) + + from . import signals # pylint: disable=unused-import diff --git a/cvat/apps/analytics_report/default_settings.py b/cvat/apps/analytics_report/default_settings.py new file mode 100644 index 000000000000..52e06e944a4d --- /dev/null +++ b/cvat/apps/analytics_report/default_settings.py @@ -0,0 +1,8 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +import os + +ANALYTICS_CHECK_JOB_DELAY = int(os.getenv("CVAT_ANALYTICS_CHECK_JOB_DELAY", 15 * 60)) +"The delay before the next analytics check job is queued, in seconds" diff --git a/cvat/apps/analytics_report/migrations/0001_initial.py b/cvat/apps/analytics_report/migrations/0001_initial.py new file mode 100644 index 000000000000..21ae6f1517f1 --- /dev/null +++ b/cvat/apps/analytics_report/migrations/0001_initial.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.1 on 2023-07-05 09:08 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("engine", "0072_alter_issue_updated_date"), + ] + + operations = [ + migrations.CreateModel( + name="AnalyticsReport", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_date", models.DateTimeField(auto_now=True)), + ("statistics", models.JSONField()), + ( + "job", + models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="analytics_report", + to="engine.job", + ), + ), + ( + "project", + models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="analytics_report", + to="engine.project", + ), + ), + ( + "task", + models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="analytics_report", + to="engine.task", + ), + ), + ], + ), + ] diff --git a/cvat/apps/analytics_report/migrations/__init__.py b/cvat/apps/analytics_report/migrations/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cvat/apps/analytics_report/models.py b/cvat/apps/analytics_report/models.py new file mode 100644 index 000000000000..b63ddb488dec --- /dev/null +++ b/cvat/apps/analytics_report/models.py @@ -0,0 +1,132 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from enum import Enum + +from django.db import models + +from cvat.apps.engine.models import Job, Project, Task + + +class TargetChoice(str, Enum): + JOB = "job" + TASK = "task" + PROJECT = "project" + + @classmethod + def choices(cls): + return tuple((x.value, x.name) for x in cls) + + @classmethod + def list(cls): + return list(map(lambda x: x.value, cls)) + + def __str__(self): + return self.value + + +class GranularityChoice(str, Enum): + DAY = "day" + WEEK = "week" + MONTH = "month" + + @classmethod + def choices(cls): + return tuple((x.value, x.name) for x in cls) + + @classmethod + def list(cls): + return list(map(lambda x: x.value, cls)) + + def __str__(self): + return self.value + + +class ViewChoice(str, Enum): + NUMERIC = "numeric" + HISTOGRAM = "histogram" + + @classmethod + def choices(cls): + return tuple((x.value, x.name) for x in cls) + + @classmethod + def list(cls): + return list(map(lambda x: x.value, cls)) + + def __str__(self): + return self.value + + +class TransformOperationType(str, Enum): + BINARY = "binary" + + @classmethod + def choices(cls): + return tuple((x.value, x.name) for x in cls) + + @classmethod + def list(cls): + return list(map(lambda x: x.value, cls)) + + def __str__(self): + return self.value + + +class BinaryOperatorType(str, Enum): + ADDITION = "+" + SUBTRACTION = "-" + MULTIPLICATION = "*" + DIVISION = "/" + + @classmethod + def choices(cls): + return tuple((x.value, x.name) for x in cls) + + @classmethod + def list(cls): + return list(map(lambda x: x.value, cls)) + + def __str__(self): + return self.value + + +class AnalyticsReport(models.Model): + job = models.OneToOneField( + Job, + on_delete=models.CASCADE, + related_name="analytics_report", + null=True, + ) + task = models.OneToOneField( + Task, + on_delete=models.CASCADE, + related_name="analytics_report", + null=True, + ) + project = models.OneToOneField( + Project, + on_delete=models.CASCADE, + related_name="analytics_report", + null=True, + ) + created_date = models.DateTimeField(auto_now=True) + statistics = models.JSONField() + + def get_task(self) -> Task: + if self.task is not None: + return self.task + else: + return self.job.segment.task + + @property + def organization_id(self): + if self.job is not None: + return getattr(self.job.segment.task.organization, "id") + elif self.task is not None: + return getattr(self.task.organization, "id", None) + elif self.project is not None: + return getattr(self.project.organization, "id", None) + + return None diff --git a/cvat/apps/analytics_report/pyproject.toml b/cvat/apps/analytics_report/pyproject.toml new file mode 100644 index 000000000000..567b78362580 --- /dev/null +++ b/cvat/apps/analytics_report/pyproject.toml @@ -0,0 +1,12 @@ +[tool.isort] +profile = "black" +forced_separate = ["tests"] +line_length = 100 +skip_gitignore = true # align tool behavior with Black +known_first_party = ["cvat"] + +# Can't just use a pyproject in the root dir, so duplicate +# https://github.com/psf/black/issues/2863 +[tool.black] +line-length = 100 +target-version = ['py38'] diff --git a/cvat/apps/analytics_report/report/__init__.py b/cvat/apps/analytics_report/report/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cvat/apps/analytics_report/report/create.py b/cvat/apps/analytics_report/report/create.py new file mode 100644 index 000000000000..3a8da9dca55a --- /dev/null +++ b/cvat/apps/analytics_report/report/create.py @@ -0,0 +1,533 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from datetime import datetime, timedelta +from typing import Union +from uuid import uuid4 + +import django_rq +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.db import transaction +from django.utils import timezone + +from cvat.apps.analytics_report.models import AnalyticsReport +from cvat.apps.analytics_report.report.derived_metrics import ( + JobTotalAnnotationSpeed, + JobTotalObjectCount, + ProjectAnnotationSpeed, + ProjectAnnotationTime, + ProjectObjects, + ProjectTotalAnnotationSpeed, + ProjectTotalObjectCount, + TaskAnnotationSpeed, + TaskAnnotationTime, + TaskObjects, + TaskTotalAnnotationSpeed, + TaskTotalObjectCount, +) +from cvat.apps.analytics_report.report.primary_metrics import ( + JobAnnotationSpeed, + JobAnnotationTime, + JobObjects, +) +from cvat.apps.engine.models import Job, Project, Task + + +def get_empty_report(): + metrics = [ + JobObjects(None), + JobAnnotationSpeed(None), + JobAnnotationTime(None), + JobTotalObjectCount(None, []), + JobTotalAnnotationSpeed(None, []), + ] + + statistics = [AnalyticsReportUpdateManager._get_empty_statistics_entry(dm) for dm in metrics] + + db_report = AnalyticsReport(statistics=statistics, created_date=datetime.now(timezone.utc)) + return db_report + + +class AnalyticsReportUpdateManager: + _QUEUE_JOB_PREFIX_TASK = "update-analytics-report-task-" + _QUEUE_JOB_PREFIX_PROJECT = "update-analytics-report-project-" + _RQ_CUSTOM_ANALYTICS_CHECK_JOB_TYPE = "custom_analytics_check" + _JOB_RESULT_TTL = 120 + + @classmethod + def _get_analytics_check_job_delay(cls) -> timedelta: + return timedelta(seconds=settings.ANALYTICS_CHECK_JOB_DELAY) + + def _get_scheduler(self): + return django_rq.get_scheduler(settings.CVAT_QUEUES.ANALYTICS_REPORTS.value) + + def _get_queue(self): + return django_rq.get_queue(settings.CVAT_QUEUES.ANALYTICS_REPORTS.value) + + def _make_queue_job_prefix(self, obj) -> str: + if isinstance(obj, Task): + return f"{self._QUEUE_JOB_PREFIX_TASK}{obj.id}-" + else: + return f"{self._QUEUE_JOB_PREFIX_PROJECT}{obj.id}-" + + def _make_custom_analytics_check_job_id(self) -> str: + return uuid4().hex + + def _make_initial_queue_job_id(self, obj) -> str: + return f"{self._make_queue_job_prefix(obj)}initial" + + def _make_regular_queue_job_id(self, obj, start_time: timezone.datetime) -> str: + return f"{self._make_queue_job_prefix(obj)}{start_time.timestamp()}" + + @classmethod + def _get_last_report_time(cls, obj): + try: + report = obj.analytics_report + if report: + return report.created_date + except ObjectDoesNotExist: + return None + + def _find_next_job_id(self, existing_job_ids, obj, *, now) -> str: + job_id_prefix = self._make_queue_job_prefix(obj) + + def _get_timestamp(job_id: str) -> timezone.datetime: + job_timestamp = job_id.split(job_id_prefix, maxsplit=1)[-1] + if job_timestamp == "initial": + return timezone.datetime.min.replace(tzinfo=timezone.utc) + else: + return timezone.datetime.fromtimestamp(float(job_timestamp), tz=timezone.utc) + + max_job_id = max( + (j for j in existing_job_ids if j.startswith(job_id_prefix)), + key=_get_timestamp, + default=None, + ) + max_timestamp = _get_timestamp(max_job_id) if max_job_id else None + + last_update_time = self._get_last_report_time(obj) + if last_update_time is None: + # Report has never been computed, is queued, or is being computed + queue_job_id = self._make_initial_queue_job_id(obj) + elif max_timestamp is not None and now < max_timestamp: + # Reuse the existing next job + queue_job_id = max_job_id + else: + # Add an updating job in the queue in the next time frame + delay = self._get_analytics_check_job_delay() + intervals = max(1, 1 + (now - last_update_time) // delay) + next_update_time = last_update_time + delay * intervals + queue_job_id = self._make_regular_queue_job_id(obj, next_update_time) + + return queue_job_id + + class AnalyticsReportsNotAvailable(Exception): + pass + + def schedule_analytics_report_autoupdate_job(self, *, job=None, task=None, project=None): + assert sum(map(bool, (job, task, project))) == 1, "Expected only 1 argument" + + now = timezone.now() + delay = self._get_analytics_check_job_delay() + next_job_time = now.utcnow() + delay + + scheduler = self._get_scheduler() + existing_job_ids = set(j.id for j in scheduler.get_jobs(until=next_job_time)) + + target_obj = None + cvat_project_id = None + cvat_task_id = None + if job is not None: + if job.segment.task.project: + target_obj = job.segment.task.project + cvat_project_id = target_obj.id + else: + target_obj = job.segment.task + cvat_task_id = target_obj.id + elif task is not None: + if task.project: + target_obj = task.project + cvat_project_id = target_obj.id + else: + target_obj = task + cvat_task_id = target_obj.id + elif project is not None: + target_obj = project + cvat_project_id = project.id + + queue_job_id = self._find_next_job_id(existing_job_ids, target_obj, now=now) + if queue_job_id not in existing_job_ids: + scheduler.enqueue_at( + next_job_time, + self._check_analytics_report, + cvat_task_id=cvat_task_id, + cvat_project_id=cvat_project_id, + job_id=queue_job_id, + ) + + def schedule_analytics_check_job(self, *, job=None, task=None, project=None, user_id): + rq_id = self._make_custom_analytics_check_job_id() + + queue = self._get_queue() + queue.enqueue( + self._check_analytics_report, + cvat_job_id=job.id if job is not None else None, + cvat_task_id=task.id if task is not None else None, + cvat_project_id=project.id if project is not None else None, + job_id=rq_id, + meta={"user_id": user_id, "job_type": self._RQ_CUSTOM_ANALYTICS_CHECK_JOB_TYPE}, + result_ttl=self._JOB_RESULT_TTL, + failure_ttl=self._JOB_RESULT_TTL, + ) + + return rq_id + + def get_analytics_check_job(self, rq_id: str): + queue = self._get_queue() + rq_job = queue.fetch_job(rq_id) + + if rq_job and not self.is_custom_analytics_check_job(rq_job): + rq_job = None + + return rq_job + + def is_custom_analytics_check_job(self, rq_job) -> bool: + return rq_job.meta.get("job_type") == self._RQ_CUSTOM_ANALYTICS_CHECK_JOB_TYPE + + @staticmethod + def _get_analytics_report(db_obj: Union[Job, Task, Project]) -> AnalyticsReport: + db_report = getattr(db_obj, "analytics_report", None) + if db_report is None: + db_report = AnalyticsReport(statistics=[]) + + if isinstance(db_obj, Job): + db_report.job_id = db_obj.id + elif isinstance(db_obj, Task): + db_report.task_id = db_obj.id + elif isinstance(db_obj, Project): + db_report.project_id = db_obj.id + + db_obj.analytics_report = db_report + + return db_report + + @classmethod + def _check_analytics_report( + cls, *, cvat_job_id: int = None, cvat_task_id: int = None, cvat_project_id: int = None + ) -> bool: + if cvat_job_id is not None: + queryset = Job.objects.select_related("analytics_report") + with transaction.atomic(): + # The Job could have been deleted during scheduling + try: + db_job = queryset.get(pk=cvat_job_id) + except Job.DoesNotExist: + return False + + db_report = cls._get_analytics_report(db_job) + + db_report = cls()._compute_report_for_job(db_job=db_job, db_report=db_report) + + with transaction.atomic(): + # The job could have been deleted during processing + try: + actual_job = queryset.get(pk=db_job.id) + except Job.DoesNotExist: + return False + + actual_report = getattr(actual_job, "analytics_report", None) + actual_created_date = ( + getattr(actual_report, "created_date", None) + if actual_report is not None + else None + ) + # The report has been updated during processing + if db_report.created_date != actual_created_date: + return False + + db_report.save() + return True + + elif cvat_task_id is not None: + queryset = Task.objects.select_related("analytics_report").prefetch_related( + "segment_set__job_set" + ) + with transaction.atomic(): + try: + db_task = queryset.get(pk=cvat_task_id) + except Task.DoesNotExist: + return False + + db_report = cls._get_analytics_report(db_task) + db_report, job_reports = cls()._compute_report_for_task( + db_task=db_task, db_report=db_report + ) + + with transaction.atomic(): + # The task could have been deleted during processing + try: + actual_task = queryset.get(pk=cvat_task_id) + except Task.DoesNotExist: + return False + + actual_report = getattr(actual_task, "analytics_report", None) + actual_created_date = ( + actual_report.created_date if actual_report is not None else None + ) + # The report has been updated during processing + if db_report.created_date != actual_created_date: + return False + + actual_job_report_created_dates = {} + for db_segment in db_task.segment_set.all(): + for db_job in db_segment.job_set.all(): + ar = getattr(db_job, "analytics_report", None) + acd = ar.created_date if ar is not None else None + actual_job_report_created_dates[db_job.id] = acd + + for jr in job_reports: + if jr.created_date != actual_job_report_created_dates[jr.job_id]: + return False + + db_report.save() + for jr in job_reports: + jr.save() + return True + + elif cvat_project_id is not None: + queryset = Project.objects.select_related("analytics_report").prefetch_related( + "tasks__segment_set__job_set" + ) + with transaction.atomic(): + try: + db_project = queryset.get(pk=cvat_project_id) + except Project.DoesNotExist: + return False + + db_report = cls._get_analytics_report(db_project) + db_report, task_reports, job_reports = cls()._compute_report_for_project( + db_project=db_project, db_report=db_report + ) + + with transaction.atomic(): + # The Project could have been deleted during processing + try: + actual_project = queryset.get(pk=cvat_project_id) + except Project.DoesNotExist: + return False + + actual_report = getattr(actual_project, "analytics_report", None) + actual_created_date = ( + actual_report.created_date if actual_report is not None else None + ) + # The report has been updated during processing + if db_report.created_date != actual_created_date: + return False + + actual_job_report_created_dates = {} + actual_tasks_report_created_dates = {} + for db_task in db_project.tasks.all(): + task_ar = getattr(db_task, "analytics_report", None) + task_ar_created_date = task_ar.created_date if task_ar else None + actual_tasks_report_created_dates[db_task.id] = task_ar_created_date + for db_segment in db_task.segment_set.all(): + for db_job in db_segment.job_set.all(): + ar = getattr(db_job, "analytics_report", None) + acd = ar.created_date if ar is not None else None + actual_job_report_created_dates[db_job.id] = acd + + for tr in task_reports: + if tr.created_date != actual_tasks_report_created_dates[tr.task_id]: + return False + + for jr in job_reports: + if jr.created_date != actual_job_report_created_dates[jr.job_id]: + return False + + db_report.save() + for tr in task_reports: + tr.save() + + for jr in job_reports: + jr.save() + return True + + @staticmethod + def _get_statistics_entry_props(statistics_object): + return { + "name": statistics_object.key(), + "title": statistics_object.title(), + "description": statistics_object.description(), + "granularity": statistics_object.granularity(), + "default_view": statistics_object.default_view(), + "transformations": statistics_object.transformations(), + "is_filterable_by_date": statistics_object.is_filterable_by_date(), + } + + @staticmethod + def _get_statistics_entry(statistics_object): + return { + **AnalyticsReportUpdateManager._get_statistics_entry_props(statistics_object), + **{"data_series": statistics_object.calculate()}, + } + + @staticmethod + def _get_empty_statistics_entry(statistics_object): + return { + **AnalyticsReportUpdateManager._get_statistics_entry_props(statistics_object), + **{"data_series": statistics_object.get_empty()}, + } + + @staticmethod + def _get_metric_by_key(key, statistics): + return next(filter(lambda s: s["name"] == key, statistics)) + + def _compute_report_for_job(self, db_job: Job, db_report: AnalyticsReport) -> AnalyticsReport: + # recalculate the report if there is no report or the existing one is outdated + if db_report.created_date is None or db_report.created_date < db_job.updated_date: + primary_metrics = [ + JobObjects(db_job), + JobAnnotationSpeed(db_job), + JobAnnotationTime(db_job), + ] + + primary_statistics = { + pm.key(): self._get_statistics_entry(pm) for pm in primary_metrics + } + + derived_metrics = [ + JobTotalObjectCount( + db_job, primary_statistics=primary_statistics[JobAnnotationSpeed.key()] + ), + JobTotalAnnotationSpeed( + db_job, primary_statistics=primary_statistics[JobAnnotationSpeed.key()] + ), + ] + + derived_statistics = { + dm.key(): self._get_statistics_entry(dm) for dm in derived_metrics + } + + db_report.statistics = [primary_statistics[pm.key()] for pm in primary_metrics] + db_report.statistics.extend(derived_statistics[dm.key()] for dm in derived_metrics) + + return db_report + + def _compute_report_for_task( + self, + db_task: Task, + db_report: AnalyticsReport, + ) -> tuple[AnalyticsReport, list[AnalyticsReport]]: + job_reports = [] + for db_segment in db_task.segment_set.all(): + for db_job in db_segment.job_set.all(): + job_report = self._get_analytics_report(db_job) + job_reports.append( + self._compute_report_for_job(db_job=db_job, db_report=job_report) + ) + # recalculate the report if there is no report or the existing one is outdated + if db_report.created_date is None or db_report.created_date < db_task.updated_date: + derived_metrics = [ + TaskObjects( + db_task, + [ + self._get_metric_by_key(JobObjects.key(), jr.statistics) + for jr in job_reports + ], + ), + TaskAnnotationSpeed( + db_task, + [ + self._get_metric_by_key(JobAnnotationSpeed.key(), jr.statistics) + for jr in job_reports + ], + ), + TaskAnnotationTime( + db_task, + [ + self._get_metric_by_key(JobAnnotationTime.key(), jr.statistics) + for jr in job_reports + ], + ), + TaskTotalObjectCount( + db_task, + [ + self._get_metric_by_key(JobAnnotationSpeed.key(), jr.statistics) + for jr in job_reports + ], + ), + TaskTotalAnnotationSpeed( + db_task, + [ + self._get_metric_by_key(JobAnnotationSpeed.key(), jr.statistics) + for jr in job_reports + ], + ), + ] + + statistics = [self._get_statistics_entry(dm) for dm in derived_metrics] + db_report.statistics = statistics + + return db_report, job_reports + + def _compute_report_for_project( + self, db_project: Project, db_report: AnalyticsReport + ) -> tuple[AnalyticsReport, list[AnalyticsReport], list[AnalyticsReport]]: + job_reports = [] + task_reports = [] + for db_task in db_project.tasks.all(): + db_task_report = self._get_analytics_report(db_task) + tr, jrs = self._compute_report_for_task(db_task, db_task_report) + task_reports.append(tr) + job_reports.extend(jrs) + # recalculate the report if there is no report or the existing one is outdated + if db_report.created_date is None or db_report.created_date < db_project.updated_date: + derived_metrics = [ + ProjectObjects( + db_project, + [ + self._get_metric_by_key(JobObjects.key(), jr.statistics) + for jr in job_reports + ], + ), + ProjectAnnotationSpeed( + db_project, + [ + self._get_metric_by_key(JobAnnotationSpeed.key(), jr.statistics) + for jr in job_reports + ], + ), + ProjectAnnotationTime( + db_project, + [ + self._get_metric_by_key(JobAnnotationTime.key(), jr.statistics) + for jr in job_reports + ], + ), + ProjectTotalObjectCount( + db_project, + [ + self._get_metric_by_key(JobAnnotationSpeed.key(), jr.statistics) + for jr in job_reports + ], + ), + ProjectTotalAnnotationSpeed( + db_project, + [ + self._get_metric_by_key(JobAnnotationSpeed.key(), jr.statistics) + for jr in job_reports + ], + ), + ] + + statistics = [self._get_statistics_entry(dm) for dm in derived_metrics] + db_report.statistics = statistics + + return db_report, task_reports, job_reports + + def _get_current_job(self): + from rq import get_current_job + + return get_current_job() diff --git a/cvat/apps/analytics_report/report/derived_metrics/__init__.py b/cvat/apps/analytics_report/report/derived_metrics/__init__.py new file mode 100644 index 000000000000..4c2875a4dbae --- /dev/null +++ b/cvat/apps/analytics_report/report/derived_metrics/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from .annotation_speed import ProjectAnnotationSpeed, TaskAnnotationSpeed +from .annotation_time import ProjectAnnotationTime, TaskAnnotationTime +from .objects import ProjectObjects, TaskObjects +from .total_annotation_speed import ( + JobTotalAnnotationSpeed, + ProjectTotalAnnotationSpeed, + TaskTotalAnnotationSpeed, +) +from .total_object_count import JobTotalObjectCount, ProjectTotalObjectCount, TaskTotalObjectCount diff --git a/cvat/apps/analytics_report/report/derived_metrics/annotation_speed.py b/cvat/apps/analytics_report/report/derived_metrics/annotation_speed.py new file mode 100644 index 000000000000..5835c3774e0f --- /dev/null +++ b/cvat/apps/analytics_report/report/derived_metrics/annotation_speed.py @@ -0,0 +1,55 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from datetime import datetime, timezone + +from dateutil import parser + +from cvat.apps.analytics_report.report.primary_metrics import JobAnnotationSpeed + +from .base import DerivedMetricBase + + +class TaskAnnotationSpeed(DerivedMetricBase, JobAnnotationSpeed): + _description = "Metric shows the annotation speed in objects per hour for the Task." + _query = None + + def calculate(self): + combined_statistics = {} + + for job_report in self._primary_statistics: + data_series = job_report["data_series"] + for oc_entry, wt_entry in zip(data_series["object_count"], data_series["working_time"]): + entry = combined_statistics.setdefault( + parser.parse(oc_entry["datetime"]).date(), + { + "object_count": 0, + "working_time": 0, + }, + ) + entry["object_count"] += oc_entry["value"] + entry["working_time"] += wt_entry["value"] + + combined_data_series = { + "object_count": [], + "working_time": [], + } + + for key in sorted(combined_statistics.keys()): + timestamp_str = datetime.combine( + key, datetime.min.time(), tzinfo=timezone.utc + ).strftime("%Y-%m-%dT%H:%M:%SZ") + for s_name in ("object_count", "working_time"): + combined_data_series[s_name].append( + { + "value": combined_statistics[key][s_name], + "datetime": timestamp_str, + } + ) + + return combined_data_series + + +class ProjectAnnotationSpeed(TaskAnnotationSpeed): + _description = "Metric shows the annotation speed in objects per hour for the Project." diff --git a/cvat/apps/analytics_report/report/derived_metrics/annotation_time.py b/cvat/apps/analytics_report/report/derived_metrics/annotation_time.py new file mode 100644 index 000000000000..42f23f642e82 --- /dev/null +++ b/cvat/apps/analytics_report/report/derived_metrics/annotation_time.py @@ -0,0 +1,31 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from cvat.apps.analytics_report.report.primary_metrics import JobAnnotationTime + +from .base import DerivedMetricBase + + +class TaskAnnotationTime(DerivedMetricBase, JobAnnotationTime): + _description = "Metric shows how long the Task is in progress state." + _query = None + + def calculate(self): + entry = {"value": 0, "datetime": self._get_utc_now().strftime("%Y-%m-%dT%H:%M:%SZ")} + for job_report in self._primary_statistics: + data_series = job_report["data_series"] + for at_entry in data_series["total_annotating_time"]: + if at_entry["value"] > entry["value"]: + entry["value"] = at_entry["value"] + entry["datetime"] = at_entry["datetime"] + + combined_data_series = { + "total_annotating_time": [entry], + } + + return combined_data_series + + +class ProjectAnnotationTime(TaskAnnotationTime): + _description = "Metric shows how long the Project is in progress state." diff --git a/cvat/apps/analytics_report/report/derived_metrics/base.py b/cvat/apps/analytics_report/report/derived_metrics/base.py new file mode 100644 index 000000000000..6d6d3d8e38c7 --- /dev/null +++ b/cvat/apps/analytics_report/report/derived_metrics/base.py @@ -0,0 +1,12 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from cvat.apps.analytics_report.report.primary_metrics import PrimaryMetricBase + + +class DerivedMetricBase(PrimaryMetricBase): + def __init__(self, db_obj, primary_statistics): + super().__init__(db_obj) + + self._primary_statistics = primary_statistics diff --git a/cvat/apps/analytics_report/report/derived_metrics/objects.py b/cvat/apps/analytics_report/report/derived_metrics/objects.py new file mode 100644 index 000000000000..07b62d4c58b5 --- /dev/null +++ b/cvat/apps/analytics_report/report/derived_metrics/objects.py @@ -0,0 +1,61 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from datetime import datetime, timezone + +from dateutil import parser + +from cvat.apps.analytics_report.report.primary_metrics import JobObjects + +from .base import DerivedMetricBase + + +class TaskObjects(DerivedMetricBase, JobObjects): + _description = "Metric shows number of added/changed/deleted objects for the Task." + _query = None + + def calculate(self): + combined_statistics = {} + + for job_report in self._primary_statistics: + data_series = job_report["data_series"] + for c_entry, u_entry, d_entry in zip( + data_series["created"], data_series["updated"], data_series["deleted"] + ): + entry = combined_statistics.setdefault( + parser.parse(c_entry["datetime"]).date(), + { + "created": 0, + "updated": 0, + "deleted": 0, + }, + ) + entry["created"] += c_entry["value"] + entry["updated"] += u_entry["value"] + entry["deleted"] += d_entry["value"] + + combined_data_series = { + "created": [], + "updated": [], + "deleted": [], + } + + for key in sorted(combined_statistics.keys()): + timestamp_str = datetime.combine( + key, datetime.min.time(), tzinfo=timezone.utc + ).strftime("%Y-%m-%dT%H:%M:%SZ") + + for action in ("created", "updated", "deleted"): + combined_data_series[action].append( + { + "value": combined_statistics[key][action], + "datetime": timestamp_str, + } + ) + + return combined_data_series + + +class ProjectObjects(TaskObjects): + _description = "Metric shows number of added/changed/deleted objects for the Project." diff --git a/cvat/apps/analytics_report/report/derived_metrics/total_annotation_speed.py b/cvat/apps/analytics_report/report/derived_metrics/total_annotation_speed.py new file mode 100644 index 000000000000..ee0b24f7edae --- /dev/null +++ b/cvat/apps/analytics_report/report/derived_metrics/total_annotation_speed.py @@ -0,0 +1,66 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from cvat.apps.analytics_report.models import ViewChoice + +from .base import DerivedMetricBase + + +class JobTotalAnnotationSpeed(DerivedMetricBase): + _title = "Total Annotation Speed (objects per hour)" + _description = "Metric shows total annotation speed in the Job." + _default_view = ViewChoice.NUMERIC + _key = "total_annotation_speed" + _is_filterable_by_date = False + + def calculate(self): + total_count = 0 + total_wt = 0 + data_series = self._primary_statistics["data_series"] + for ds in zip(data_series["object_count"], data_series["working_time"]): + total_count += ds[0]["value"] + total_wt += ds[1]["value"] + + metric = self.get_empty() + metric["total_annotation_speed"][0]["value"] = ( + total_count / max(total_wt, 1) if total_wt != 0 else 0 + ) + return metric + + def get_empty(self): + return { + "total_annotation_speed": [ + { + "value": 0, + "datetime": self._get_utc_now().strftime("%Y-%m-%dT%H:%M:%SZ"), + }, + ] + } + + +class TaskTotalAnnotationSpeed(JobTotalAnnotationSpeed): + _description = "Metric shows total annotation speed in the Task." + + def calculate(self): + total_count = 0 + total_wt = 0 + + for job_report in self._primary_statistics: + data_series = job_report["data_series"] + for oc_entry, wt_entry in zip(data_series["object_count"], data_series["working_time"]): + total_count += oc_entry["value"] + total_wt += wt_entry["value"] + + return { + "total_annotation_speed": [ + { + "value": total_count / max(total_wt, 1) if total_wt != 0 else 0, + "datetime": self._get_utc_now().strftime("%Y-%m-%dT%H:%M:%SZ"), + }, + ] + } + + +class ProjectTotalAnnotationSpeed(TaskTotalAnnotationSpeed): + _description = "Metric shows total annotation speed in the Project." diff --git a/cvat/apps/analytics_report/report/derived_metrics/total_object_count.py b/cvat/apps/analytics_report/report/derived_metrics/total_object_count.py new file mode 100644 index 000000000000..7890221f5901 --- /dev/null +++ b/cvat/apps/analytics_report/report/derived_metrics/total_object_count.py @@ -0,0 +1,59 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from cvat.apps.analytics_report.models import ViewChoice + +from .base import DerivedMetricBase + + +class JobTotalObjectCount(DerivedMetricBase): + _title = "Total Object Count" + _description = "Metric shows total object count in the Job." + _default_view = ViewChoice.NUMERIC + _key = "total_object_count" + _is_filterable_by_date = False + + def calculate(self): + count = 0 + data_series = self._primary_statistics["data_series"] + for ds in data_series["object_count"]: + count += ds["value"] + + metric = self.get_empty() + metric["total_object_count"][0]["value"] = count + return metric + + def get_empty(self): + return { + "total_object_count": [ + { + "value": 0, + "datetime": self._get_utc_now().strftime("%Y-%m-%dT%H:%M:%SZ"), + }, + ] + } + + +class TaskTotalObjectCount(JobTotalObjectCount): + _description = "Metric shows total object count in the Task." + + def calculate(self): + total_count = 0 + for job_report in self._primary_statistics: + data_series = job_report["data_series"] + for oc_entry in data_series["object_count"]: + total_count += oc_entry["value"] + + return { + "total_object_count": [ + { + "value": total_count, + "datetime": self._get_utc_now().strftime("%Y-%m-%dT%H:%M:%SZ"), + }, + ] + } + + +class ProjectTotalObjectCount(TaskTotalObjectCount): + _description = "Metric shows total object count in the Project." diff --git a/cvat/apps/analytics_report/report/get.py b/cvat/apps/analytics_report/report/get.py new file mode 100644 index 000000000000..82622e5921a1 --- /dev/null +++ b/cvat/apps/analytics_report/report/get.py @@ -0,0 +1,164 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from datetime import datetime, timedelta, timezone + +from dateutil import parser +from rest_framework import serializers, status +from rest_framework.exceptions import NotFound +from rest_framework.response import Response + +from cvat.apps.analytics_report.models import AnalyticsReport, TargetChoice +from cvat.apps.analytics_report.report.create import get_empty_report +from cvat.apps.analytics_report.serializers import AnalyticsReportSerializer +from cvat.apps.engine.models import Job, Project, Task + + +def _filter_statistics_by_date(statistics, start_date, end_date): + for metric in statistics: + data_series = metric.get("data_series", {}) + if metric.get("is_filterable_by_date", False): + for ds_name, ds_entry in data_series.items(): + data_series[ds_name] = list( + filter( + lambda df: start_date <= parser.parse(df["datetime"]) <= end_date, ds_entry + ) + ) + + return statistics + + +def _convert_datetime_to_date(statistics): + for metric in statistics: + data_series = metric.get("data_series", {}) + for ds_entry in data_series.values(): + for df in ds_entry: + df["date"] = parser.parse(df["datetime"]).date() + del df["datetime"] + return statistics + + +def _clamp_working_time(statistics): + affected_metrics = "annotation_speed" + for metric in statistics: + if metric["name"] not in affected_metrics: + continue + data_series = metric.get("data_series", {}) + if data_series: + for df in data_series["working_time"]: + df["value"] = max(df["value"], 1) + + return statistics + + +def _get_object_report(obj_model, pk, start_date, end_date): + try: + db_obj = obj_model.objects.get(pk=pk) + db_analytics_report = db_obj.analytics_report + except obj_model.DoesNotExist as ex: + raise NotFound(f"{obj_model.__class__.__name__} object with pk={pk} does not exist") from ex + except AnalyticsReport.DoesNotExist: + db_analytics_report = get_empty_report() + + statistics = _filter_statistics_by_date(db_analytics_report.statistics, start_date, end_date) + statistics = _convert_datetime_to_date(statistics) + statistics = _clamp_working_time(statistics) + + if obj_model is Job: + target = TargetChoice.JOB + elif obj_model is Task: + target = TargetChoice.TASK + elif obj_model is Project: + target = TargetChoice.PROJECT + + data = { + "target": target, + f"{obj_model.__name__.lower()}_id": pk, + "statistics": statistics, + "created_date": db_analytics_report.created_date, + } + return data + + +def _get_job_report(job_id, start_date, end_date): + return _get_object_report(Job, int(job_id), start_date, end_date) + + +def _get_task_report(task_id, start_date, end_date): + return _get_object_report(Task, int(task_id), start_date, end_date) + + +def _get_project_report(project_id, start_date, end_date): + return _get_object_report(Project, int(project_id), start_date, end_date) + + +def get_analytics_report(request, query_params): + query_params = { + "project_id": query_params.get("project_id", None), + "task_id": query_params.get("task_id", None), + "job_id": query_params.get("job_id", None), + "start_date": query_params.get("start_date", None), + "end_date": query_params.get("end_date", None), + } + + try: + if query_params["start_date"]: + query_params["start_date"] = parser.parse(query_params["start_date"]) + except parser.ParserError: + raise serializers.ValidationError( + f"Cannot parse 'start_date' datetime parameter: {query_params['start_date']}" + ) + try: + if query_params["end_date"]: + query_params["end_date"] = parser.parse(query_params["end_date"]) + except parser.ParserError: + raise serializers.ValidationError( + f"Cannot parse 'end_date' datetime parameter: {query_params['end_date']}" + ) + + if ( + query_params["start_date"] + and query_params["end_date"] + and query_params["start_date"] > query_params["end_date"] + ): + raise serializers.ValidationError("'start_date' must be before than 'end_date'") + + # Set the default time interval to last 30 days + if not query_params["start_date"] and not query_params["end_date"]: + query_params["end_date"] = datetime.now(timezone.utc) + query_params["start_date"] = query_params["end_date"] - timedelta(days=30) + elif query_params["start_date"] and not query_params["end_date"]: + query_params["end_date"] = datetime.now(timezone.utc) + elif not query_params["start_date"] and query_params["end_date"]: + query_params["end_date"] = datetime.min + + job_id = query_params.get("job_id", None) + task_id = query_params.get("task_id", None) + project_id = query_params.get("project_id", None) + + if job_id is None and task_id is None and project_id is None: + raise serializers.ValidationError("No any job, task or project specified") + + if sum(map(bool, [job_id, task_id, project_id])) > 1: + raise serializers.ValidationError( + "Only one of job_id, task_id or project_id must be specified" + ) + + report = None + try: + if job_id is not None: + report = _get_job_report(job_id, query_params["start_date"], query_params["end_date"]) + elif task_id is not None: + report = _get_task_report(task_id, query_params["start_date"], query_params["end_date"]) + elif project_id is not None: + report = _get_project_report( + project_id, query_params["start_date"], query_params["end_date"] + ) + except AnalyticsReport.DoesNotExist: + return Response("Analytics report not found", status=status.HTTP_404_NOT_FOUND) + + serializer = AnalyticsReportSerializer(data=report) + serializer.is_valid(raise_exception=True) + + return Response(serializer.data) diff --git a/cvat/apps/analytics_report/report/primary_metrics/__init__.py b/cvat/apps/analytics_report/report/primary_metrics/__init__.py new file mode 100644 index 000000000000..36c9c6be52e4 --- /dev/null +++ b/cvat/apps/analytics_report/report/primary_metrics/__init__.py @@ -0,0 +1,8 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from .annotation_speed import JobAnnotationSpeed +from .annotation_time import JobAnnotationTime +from .base import PrimaryMetricBase +from .objects import JobObjects diff --git a/cvat/apps/analytics_report/report/primary_metrics/annotation_speed.py b/cvat/apps/analytics_report/report/primary_metrics/annotation_speed.py new file mode 100644 index 000000000000..98a273451bde --- /dev/null +++ b/cvat/apps/analytics_report/report/primary_metrics/annotation_speed.py @@ -0,0 +1,136 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from dateutil import parser + +import cvat.apps.dataset_manager as dm +from cvat.apps.analytics_report.models import ( + BinaryOperatorType, + GranularityChoice, + TransformOperationType, + ViewChoice, +) +from cvat.apps.analytics_report.report.primary_metrics.base import PrimaryMetricBase +from cvat.apps.engine.models import SourceType + + +class JobAnnotationSpeed(PrimaryMetricBase): + _title = "Annotation speed (objects per hour)" + _description = "Metric shows the annotation speed in objects per hour." + _default_view = ViewChoice.HISTOGRAM + _key = "annotation_speed" + # Raw SQL queries are used to execute ClickHouse queries, as there is no ORM available here + _query = "SELECT sum(JSONExtractUInt(payload, 'working_time')) / 1000 / 3600 as wt FROM events WHERE job_id={job_id:UInt64} AND timestamp >= {start_datetime:DateTime64} AND timestamp < {end_datetime:DateTime64}" + _granularity = GranularityChoice.DAY + _is_filterable_by_date = False + _transformations = [ + { + "name": "annotation_speed", + TransformOperationType.BINARY: { + "left": "object_count", + "operator": BinaryOperatorType.DIVISION, + "right": "working_time", + }, + }, + ] + + def calculate(self): + def get_tags_count(annotations): + return sum(1 for t in annotations["tags"] if t["source"] != SourceType.FILE) + + def get_shapes_count(annotations): + return sum(1 for s in annotations["shapes"] if s["source"] != SourceType.FILE) + + def get_track_count(annotations): + count = 0 + for track in annotations["tracks"]: + if track["source"] == SourceType.FILE: + continue + if len(track["shapes"]) == 1: + count += self._db_obj.segment.stop_frame - track["shapes"][0]["frame"] + 1 + for prev_shape, cur_shape in zip(track["shapes"], track["shapes"][1:]): + if prev_shape["outside"] is not True: + count += cur_shape["frame"] - prev_shape["frame"] + + return count + + def get_default(): + return { + "data_series": { + "object_count": [], + "working_time": [], + } + } + + # Calculate object count + + annotations = dm.task.get_job_data(self._db_obj.id) + object_count = 0 + object_count += get_tags_count(annotations) + object_count += get_shapes_count(annotations) + object_count += get_track_count(annotations) + + timestamp = self._get_utc_now() + timestamp_str = timestamp.strftime("%Y-%m-%dT%H:%M:%SZ") + + report = self._db_obj.analytics_report + if report is None: + statistics = get_default() + else: + statistics = next( + filter(lambda s: s["name"] == "annotation_speed", report.statistics), get_default() + ) + + data_series = statistics["data_series"] + + last_entry_count = 0 + start_datetime = self._db_obj.created_date + if data_series["object_count"]: + last_entry = data_series["object_count"][-1] + last_entry_timestamp = parser.parse(last_entry["datetime"]) + + if last_entry_timestamp.date() == timestamp.date(): + data_series["object_count"] = data_series["object_count"][:-1] + data_series["working_time"] = data_series["working_time"][:-1] + if len(data_series["object_count"]): + last_last_entry = data_series["object_count"][-1] + start_datetime = parser.parse(last_last_entry["datetime"]) + last_entry_count = last_last_entry["value"] + else: + last_entry_count = last_entry["value"] + start_datetime = parser.parse(last_entry["datetime"]) + + data_series["object_count"].append( + { + "value": object_count - last_entry_count, + "datetime": timestamp_str, + } + ) + + # Calculate working time + + parameters = { + "job_id": self._db_obj.id, + "start_datetime": start_datetime, + "end_datetime": self._get_utc_now(), + } + + result = self._make_clickhouse_query(parameters) + value = 0 + if (wt := next(iter(result.result_rows))[0]) is not None: + value = wt + data_series["working_time"].append( + { + "value": value, + "datetime": timestamp_str, + } + ) + + return data_series + + def get_empty(self): + return { + "object_count": [], + "working_time": [], + } diff --git a/cvat/apps/analytics_report/report/primary_metrics/annotation_time.py b/cvat/apps/analytics_report/report/primary_metrics/annotation_time.py new file mode 100644 index 000000000000..919766e0ffaf --- /dev/null +++ b/cvat/apps/analytics_report/report/primary_metrics/annotation_time.py @@ -0,0 +1,54 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from cvat.apps.analytics_report.models import ViewChoice +from cvat.apps.analytics_report.report.primary_metrics.base import PrimaryMetricBase + + +class JobAnnotationTime(PrimaryMetricBase): + _title = "Annotation time (hours)" + _description = "Metric shows how long the Job is in progress state." + _default_view = ViewChoice.NUMERIC + _key = "annotation_time" + # Raw SQL queries are used to execute ClickHouse queries, as there is no ORM available here + _query = "SELECT timestamp, obj_val FROM cvat.events WHERE scope='update:job' AND job_id={job_id:UInt64} AND obj_name='state' ORDER BY timestamp ASC" + _is_filterable_by_date = False + + def calculate(self): + results = self._make_clickhouse_query( + { + "job_id": self._db_obj.id, + } + ) + total_annotating_time = 0 + last_change = None + for prev_row, cur_row in zip(results.result_rows, results.result_rows[1:]): + if prev_row[1] == "in progress": + total_annotating_time += int((cur_row[0] - prev_row[0]).total_seconds()) + last_change = cur_row[0] + + if results.result_rows and results.result_rows[-1][1] == "in progress": + total_annotating_time += int( + (self._db_obj.updated_date - results.result_rows[-1][0]).total_seconds() + ) + + if not last_change: + last_change = self._get_utc_now() + + metric = self.get_empty() + metric["total_annotating_time"][0]["value"] = ( + total_annotating_time / 3600 + ) # convert to hours + metric["total_annotating_time"][0]["datetime"] = last_change.strftime("%Y-%m-%dT%H:%M:%SZ") + return metric + + def get_empty(self): + return { + "total_annotating_time": [ + { + "value": 0, + "datetime": self._get_utc_now().strftime("%Y-%m-%dT%H:%M:%SZ"), + }, + ] + } diff --git a/cvat/apps/analytics_report/report/primary_metrics/base.py b/cvat/apps/analytics_report/report/primary_metrics/base.py new file mode 100644 index 000000000000..eb8321862fa4 --- /dev/null +++ b/cvat/apps/analytics_report/report/primary_metrics/base.py @@ -0,0 +1,66 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from abc import ABCMeta, abstractmethod +from datetime import datetime, timezone + +from cvat.apps.analytics_report.report.primary_metrics.utils import make_clickhouse_query + + +class PrimaryMetricBase(metaclass=ABCMeta): + _title = None + _description = None + # Raw SQL queries are used to execute ClickHouse queries, as there is no ORM available here + _query = None + _granularity = None + _default_view = None + _key = None + _transformations = [] + _is_filterable_by_date = True + + def __init__(self, db_obj): + self._db_obj = db_obj + + @classmethod + def description(cls): + return cls._description + + @classmethod + def title(cls): + return cls._title + + @classmethod + def granularity(cls): + return cls._granularity + + @classmethod + def default_view(cls): + return cls._default_view + + @classmethod + def transformations(cls): + return cls._transformations + + @classmethod + def key(cls): + return cls._key + + @classmethod + def is_filterable_by_date(cls): + return cls._is_filterable_by_date + + @abstractmethod + def calculate(self): + ... + + @abstractmethod + def get_empty(self): + ... + + def _make_clickhouse_query(self, parameters): + return make_clickhouse_query(query=self._query, parameters=parameters) + + @staticmethod + def _get_utc_now(): + return datetime.now(timezone.utc) diff --git a/cvat/apps/analytics_report/report/primary_metrics/objects.py b/cvat/apps/analytics_report/report/primary_metrics/objects.py new file mode 100644 index 000000000000..d7d369ca146e --- /dev/null +++ b/cvat/apps/analytics_report/report/primary_metrics/objects.py @@ -0,0 +1,58 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from cvat.apps.analytics_report.models import GranularityChoice, ViewChoice +from cvat.apps.analytics_report.report.primary_metrics.base import PrimaryMetricBase + + +class JobObjects(PrimaryMetricBase): + _title = "Objects" + _description = "Metric shows number of added/changed/deleted objects for the Job." + _default_view = ViewChoice.HISTOGRAM + _key = "objects" + # Raw SQL queries are used to execute ClickHouse queries, as there is no ORM available here + _query = "SELECT toStartOfDay(timestamp) as day, sum(JSONLength(JSONExtractString(payload, {object_type:String}))) as s FROM events WHERE scope = {scope:String} AND job_id = {job_id:UInt64} GROUP BY day ORDER BY day ASC" + _granularity = GranularityChoice.DAY + + def calculate(self): + statistics = {} + + for action in ["create", "update", "delete"]: + action_data = statistics.setdefault(f"{action}d", {}) + for obj_type in ["tracks", "shapes", "tags"]: + result = self._make_clickhouse_query( + { + "scope": f"{action}:{obj_type}", + "object_type": obj_type, + "job_id": self._db_obj.id, + } + ) + action_data[obj_type] = {entry[0]: entry[1] for entry in result.result_rows} + + objects_statistics = self.get_empty() + + dates = set() + for action in ["created", "updated", "deleted"]: + for obj in ["tracks", "shapes", "tags"]: + dates.update(statistics[action][obj].keys()) + + for action in ["created", "updated", "deleted"]: + for date in sorted(dates): + objects_statistics[action].append( + { + "value": sum( + statistics[action][t].get(date, 0) for t in ["tracks", "shapes", "tags"] + ), + "datetime": date.isoformat() + "Z", + } + ) + + return objects_statistics + + def get_empty(self): + return { + "created": [], + "updated": [], + "deleted": [], + } diff --git a/cvat/apps/analytics_report/report/primary_metrics/utils.py b/cvat/apps/analytics_report/report/primary_metrics/utils.py new file mode 100644 index 000000000000..e7894d434749 --- /dev/null +++ b/cvat/apps/analytics_report/report/primary_metrics/utils.py @@ -0,0 +1,21 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +import clickhouse_connect +from django.conf import settings + + +def make_clickhouse_query(query, parameters): + clickhouse_settings = settings.CLICKHOUSE["events"] + + with clickhouse_connect.get_client( + host=clickhouse_settings["HOST"], + database=clickhouse_settings["NAME"], + port=clickhouse_settings["PORT"], + username=clickhouse_settings["USER"], + password=clickhouse_settings["PASSWORD"], + ) as client: + result = client.query(query, parameters=parameters) + + return result diff --git a/cvat/apps/analytics_report/serializers.py b/cvat/apps/analytics_report/serializers.py new file mode 100644 index 000000000000..309119dbd405 --- /dev/null +++ b/cvat/apps/analytics_report/serializers.py @@ -0,0 +1,83 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from drf_spectacular.utils import extend_schema_serializer +from rest_framework import serializers + +from cvat.apps.analytics_report.models import ( + BinaryOperatorType, + GranularityChoice, + TargetChoice, + ViewChoice, +) + + +class BinaryOperationSerializer(serializers.Serializer): + left = serializers.CharField( + required=False, + allow_null=True, + help_text="The name of the data series used as the left (first) operand of the binary operation.", + ) + operator = serializers.ChoiceField(choices=BinaryOperatorType.choices()) + right = serializers.CharField( + required=False, + allow_null=True, + help_text="The name of the data series used as the right (second) operand of the binary operation.", + ) + + +class TransformationSerializer(serializers.Serializer): + name = serializers.CharField() + binary = BinaryOperationSerializer( + required=False, + allow_null=True, + ) + + +class DataFrameSerializer(serializers.Serializer): + value = serializers.FloatField() + date = serializers.DateField() + + +class MetricSerializer(serializers.Serializer): + name = serializers.CharField() + title = serializers.CharField() + description = serializers.CharField(allow_blank=True) + granularity = serializers.ChoiceField( + choices=GranularityChoice.choices(), required=False, allow_null=True + ) + default_view = serializers.ChoiceField(choices=ViewChoice.choices()) + data_series = serializers.DictField(child=DataFrameSerializer(many=True)) + transformations = serializers.ListField(child=TransformationSerializer()) + + +@extend_schema_serializer(many=False) +class AnalyticsReportSerializer(serializers.Serializer): + created_date = serializers.DateTimeField() + target = serializers.ChoiceField(choices=TargetChoice.choices()) + job_id = serializers.IntegerField(required=False) + task_id = serializers.IntegerField(required=False) + project_id = serializers.IntegerField(required=False) + statistics = serializers.ListField(child=MetricSerializer()) + + +class AnalyticsReportCreateSerializer(serializers.Serializer): + job_id = serializers.IntegerField(required=False) + task_id = serializers.IntegerField(required=False) + project_id = serializers.IntegerField(required=False) + + def validate(self, data): + job_id = data.get("job_id") + task_id = data.get("task_id") + project_id = data.get("project_id") + + if job_id is None and task_id is None and project_id is None: + raise serializers.ValidationError("No any job, task or project specified") + + if sum(map(bool, [job_id, task_id, project_id])) > 1: + raise serializers.ValidationError( + "Only one of job_id, task_id or project_id must be specified" + ) + + return data diff --git a/cvat/apps/analytics_report/signals.py b/cvat/apps/analytics_report/signals.py new file mode 100644 index 000000000000..0b7f86a02e0f --- /dev/null +++ b/cvat/apps/analytics_report/signals.py @@ -0,0 +1,30 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from django.db.models.signals import post_save +from django.dispatch import receiver + +from cvat.apps.analytics_report.report.create import AnalyticsReportUpdateManager +from cvat.apps.engine.models import Annotation, Job, Project, Task + + +@receiver(post_save, sender=Job, dispatch_uid=__name__ + ".save_job-update_analytics_report") +@receiver(post_save, sender=Task, dispatch_uid=__name__ + ".save_task-update_analytics_report") +@receiver( + post_save, sender=Project, dispatch_uid=__name__ + ".save_project-update_analytics_report" +) +@receiver( + post_save, sender=Annotation, dispatch_uid=__name__ + ".save_annotation-update_analytics_report" +) +def __save_job__update_analytics_report(instance, created, **kwargs): + if isinstance(instance, Project): + AnalyticsReportUpdateManager().schedule_analytics_report_autoupdate_job(project=instance) + elif isinstance(instance, Task): + AnalyticsReportUpdateManager().schedule_analytics_report_autoupdate_job(task=instance) + elif isinstance(instance, Job): + AnalyticsReportUpdateManager().schedule_analytics_report_autoupdate_job(job=instance) + elif isinstance(instance, Annotation): + AnalyticsReportUpdateManager().schedule_analytics_report_autoupdate_job(job=instance.job) + else: + assert False diff --git a/cvat/apps/analytics_report/urls.py b/cvat/apps/analytics_report/urls.py new file mode 100644 index 000000000000..52f96eb4fa96 --- /dev/null +++ b/cvat/apps/analytics_report/urls.py @@ -0,0 +1,12 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from rest_framework import routers + +from . import views + +router = routers.DefaultRouter(trailing_slash=False) +router.register("analytics/reports", views.AnalyticsReportViewSet, basename="analytics_reports") + +urlpatterns = router.urls diff --git a/cvat/apps/analytics_report/views.py b/cvat/apps/analytics_report/views.py new file mode 100644 index 000000000000..9e0f81b39e0a --- /dev/null +++ b/cvat/apps/analytics_report/views.py @@ -0,0 +1,197 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +import textwrap + +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema +from rest_framework import status, viewsets +from rest_framework.exceptions import NotFound, ValidationError +from rest_framework.response import Response + +from cvat.apps.analytics_report.models import AnalyticsReport +from cvat.apps.analytics_report.report.create import AnalyticsReportUpdateManager +from cvat.apps.analytics_report.report.get import get_analytics_report +from cvat.apps.analytics_report.serializers import ( + AnalyticsReportCreateSerializer, + AnalyticsReportSerializer, +) +from cvat.apps.engine.models import Job, Project, Task +from cvat.apps.engine.serializers import RqIdSerializer + + +class AnalyticsReportViewSet(viewsets.ViewSet): + serializer_class = None + + def get_queryset(self): + return AnalyticsReport.objects + + @extend_schema( + operation_id="analytics_create_report", + summary="Creates a analytics report asynchronously and allows to check request status", + parameters=[ + OpenApiParameter( + "rq_id", + type=str, + description=textwrap.dedent( + """\ + The report creation request id. Can be specified to check the report + creation status. + """ + ), + ), + ], + request=AnalyticsReportCreateSerializer(), + responses={ + "201": OpenApiResponse( + description="A analytics report request has been computed", + ), + "202": OpenApiResponse( + RqIdSerializer, + description=textwrap.dedent( + """\ + A analytics report request has been enqueued, the request id is returned. + The request status can be checked at this endpoint by passing the {} + as the query parameter. If the request id is specified, this response + means the analytics report request is queued or is being processed. + """.format( + "rq_id" + ) + ), + ), + "400": OpenApiResponse( + description="Invalid or failed request, check the response data for details" + ), + }, + ) + def create(self, request, *args, **kwargs): + rq_id = request.query_params.get("rq_id", None) + + if rq_id is None: + input_serializer = AnalyticsReportCreateSerializer(data=request.data) + input_serializer.is_valid(raise_exception=True) + + job_id = input_serializer.validated_data.get("job_id") + task_id = input_serializer.validated_data.get("task_id") + project_id = input_serializer.validated_data.get("project_id") + + if job_id is not None: + try: + job = Job.objects.get(pk=int(job_id)) + except Job.DoesNotExist as ex: + raise NotFound(f"Job {job_id} does not exist") from ex + + try: + rq_id = AnalyticsReportUpdateManager().schedule_analytics_check_job( + job=job, user_id=request.user.id + ) + serializer = RqIdSerializer({"rq_id": rq_id}) + return Response(serializer.data, status=status.HTTP_202_ACCEPTED) + except AnalyticsReportUpdateManager.AnalyticsReportsNotAvailable as ex: + raise ValidationError(str(ex)) + elif task_id is not None: + try: + task = Task.objects.get(pk=int(task_id)) + except Task.DoesNotExist as ex: + raise NotFound(f"Task {task_id} does not exist") from ex + + try: + rq_id = AnalyticsReportUpdateManager().schedule_analytics_check_job( + task=task, user_id=request.user.id + ) + serializer = RqIdSerializer({"rq_id": rq_id}) + return Response(serializer.data, status=status.HTTP_202_ACCEPTED) + except AnalyticsReportUpdateManager.AnalyticsReportsNotAvailable as ex: + raise ValidationError(str(ex)) + elif project_id is not None: + try: + project = Project.objects.get(pk=int(project_id)) + except Project.DoesNotExist as ex: + raise NotFound(f"Project {project_id} does not exist") from ex + try: + rq_id = AnalyticsReportUpdateManager().schedule_analytics_check_job( + project=project, user_id=request.user.id + ) + serializer = RqIdSerializer({"rq_id": rq_id}) + return Response(serializer.data, status=status.HTTP_202_ACCEPTED) + except AnalyticsReportUpdateManager.AnalyticsReportsNotAvailable as ex: + raise ValidationError(str(ex)) + else: + serializer = RqIdSerializer(data={"rq_id": rq_id}) + serializer.is_valid(raise_exception=True) + rq_id = serializer.validated_data["rq_id"] + + report_manager = AnalyticsReportUpdateManager() + rq_job = report_manager.get_analytics_check_job(rq_id) + + if rq_job is None: + raise NotFound("Unknown request id") + + if rq_job.is_failed: + message = str(rq_job.exc_info) + rq_job.delete() + raise ValidationError(message) + elif rq_job.is_queued or rq_job.is_started: + return Response(status=status.HTTP_202_ACCEPTED) + elif rq_job.is_finished: + return_value = rq_job.return_value + rq_job.delete() + if not return_value: + raise ValidationError("No report has been computed") + + serializer = RqIdSerializer({"rq_id": rq_id}) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + @extend_schema( + summary="Method returns analytics report", + methods=["GET"], + operation_id="analytics_get_reports", + description="Receive analytics report", + parameters=[ + OpenApiParameter( + "project_id", + location=OpenApiParameter.QUERY, + type=OpenApiTypes.INT, + required=False, + description="Specify project ID", + ), + OpenApiParameter( + "task_id", + location=OpenApiParameter.QUERY, + type=OpenApiTypes.INT, + required=False, + description="Specify task ID", + ), + OpenApiParameter( + "job_id", + location=OpenApiParameter.QUERY, + type=OpenApiTypes.INT, + required=False, + description="Specify job ID", + ), + OpenApiParameter( + "start_date", + location=OpenApiParameter.QUERY, + type=OpenApiTypes.DATETIME, + required=False, + description="Specify a start date for filtering report data.", + ), + OpenApiParameter( + "end_date", + location=OpenApiParameter.QUERY, + type=OpenApiTypes.DATETIME, + required=False, + description="Specify the end date for filtering report data.", + ), + ], + responses={ + "200": AnalyticsReportSerializer, + "404": OpenApiResponse(description="Not found"), + }, + ) + def list(self, request): + return get_analytics_report( + request=request, + query_params=request.query_params, + ) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 4eefaa115e62..6236fbbe0d1d 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -1977,7 +1977,7 @@ def reduce_fn(acc, v): track_id = ann.attributes.pop('track_id', None) source = ann.attributes.pop('source').lower() \ - if ann.attributes.get('source', '').lower() in {'auto', 'semi-auto', 'manual'} else 'manual' + if ann.attributes.get('source', '').lower() in {'auto', 'semi-auto', 'manual', 'file'} else 'manual' shape_type = shapes[ann.type] if track_id is None or 'keyframe' not in ann.attributes or dm_dataset.format not in ['cvat', 'datumaro', 'sly_pointcloud']: @@ -1991,7 +1991,7 @@ def reduce_fn(acc, v): element_occluded = element.visibility[0] == dm.Points.Visibility.hidden element_outside = element.visibility[0] == dm.Points.Visibility.absent element_source = element.attributes.pop('source').lower() \ - if element.attributes.get('source', '').lower() in {'auto', 'semi-auto', 'manual'} else 'manual' + if element.attributes.get('source', '').lower() in {'auto', 'semi-auto', 'manual', 'file'} else 'manual' elements.append(instance_data.LabeledShape( type=shapes[element.type], frame=frame_number, @@ -2065,7 +2065,7 @@ def reduce_fn(acc, v): for n, v in element.attributes.items() ] element_source = element.attributes.pop('source').lower() \ - if element.attributes.get('source', '').lower() in {'auto', 'semi-auto', 'manual'} else 'manual' + if element.attributes.get('source', '').lower() in {'auto', 'semi-auto', 'manual', 'file'} else 'manual' tracks[track_id]['elements'][element.label].shapes.append(instance_data.TrackedShape( type=shapes[element.type], frame=frame_number, diff --git a/cvat/apps/dataset_manager/formats/cvat.py b/cvat/apps/dataset_manager/formats/cvat.py index c3a7f708aeec..53f71c0428bf 100644 --- a/cvat/apps/dataset_manager/formats/cvat.py +++ b/cvat/apps/dataset_manager/formats/cvat.py @@ -1151,7 +1151,7 @@ def load_anno(file_object, annotations): track = annotations.Track( label=el.attrib['label'], group=int(el.attrib.get('group_id', 0)), - source=el.attrib.get('source', 'manual'), + source='file', shapes=[], elements=[], ) @@ -1176,7 +1176,7 @@ def load_anno(file_object, annotations): track_elements[el.attrib['label']] = annotations.Track( label=el.attrib['label'], group=0, - source=el.attrib.get('source', 'manual'), + source='file', shapes=[], elements=[], ) @@ -1200,7 +1200,7 @@ def load_anno(file_object, annotations): 'label': el.attrib['label'], 'group': int(el.attrib.get('group_id', 0)), 'attributes': attributes, - 'source': str(el.attrib.get('source', 'manual')) + 'source': 'file', } elif ev == 'end': if el.tag == 'attribute' and elem_attributes is not None and shape_element is not None: @@ -1254,7 +1254,7 @@ def load_anno(file_object, annotations): if track is None: shape_element['frame'] = frame_id - shape_element['source'] = str(el.attrib.get('source', 'manual')) + shape_element['source'] = 'file' shape['elements'].append(annotations.LabeledShape(**shape_element)) else: shape_element["frame"] = shape['frame'] @@ -1272,7 +1272,7 @@ def load_anno(file_object, annotations): shape['frame'] = frame_id shape['label'] = el.attrib['label'] shape['group'] = int(el.attrib.get('group_id', 0)) - shape['source'] = str(el.attrib.get('source', 'manual')) + shape['source'] = 'file' shape['outside'] = False shape['occluded'] = el.attrib.get('occluded', "0") == '1' diff --git a/cvat/apps/dataset_manager/task.py b/cvat/apps/dataset_manager/task.py index 66fd499a6002..81a49d6a26ff 100644 --- a/cvat/apps/dataset_manager/task.py +++ b/cvat/apps/dataset_manager/task.py @@ -17,6 +17,7 @@ from cvat.apps.engine import models, serializers from cvat.apps.engine.plugins import plugin_decorator +from cvat.apps.events.handlers import handle_annotations_change from cvat.apps.profiler import silk_profile from cvat.apps.dataset_manager.annotation import AnnotationIR, AnnotationManager @@ -160,6 +161,7 @@ def _add_missing_shape(self, track, first_shape): missing_shape = deepcopy(first_shape) missing_shape["frame"] = track["frame"] missing_shape["outside"] = True + missing_shape.pop("id") track["shapes"].append(missing_shape) def _correct_frame_of_tracked_shapes(self, track): @@ -406,18 +408,26 @@ def _create(self, data): def create(self, data): self._create(data) + handle_annotations_change(self.db_job, self.data, "create") def put(self, data): - self._delete() + deleted_data = self._delete() + handle_annotations_change(self.db_job, deleted_data, "delete") self._create(data) + handle_annotations_change(self.db_job, self.data, "create") + def update(self, data): self._delete(data) self._create(data) + handle_annotations_change(self.db_job, self.data, "update") def _delete(self, data=None): deleted_shapes = 0 + deleted_data = {} if data is None: + self.init_from_db() + deleted_data = self.data deleted_shapes += self.db_job.labeledimage_set.all().delete()[0] deleted_shapes += self.db_job.labeledshape_set.all().delete()[0] deleted_shapes += self.db_job.labeledtrack_set.all().delete()[0] @@ -443,11 +453,20 @@ def _delete(self, data=None): deleted_shapes += labeledshape_set.delete()[0] deleted_shapes += labeledtrack_set.delete()[0] + deleted_data = { + "tags": data["tags"], + "shapes": data["shapes"], + "tracks": data["tracks"], + } + if deleted_shapes: self._set_updated_date() + return deleted_data + def delete(self, data=None): - self._delete(data) + deleted_data = self._delete(data) + handle_annotations_change(self.db_job, deleted_data, "delete") @staticmethod def _extend_attributes(attributeval_set, default_attribute_values): diff --git a/cvat/apps/dataset_manager/tests/test_formats.py b/cvat/apps/dataset_manager/tests/test_formats.py index e1fa84d874d3..c710013d547d 100644 --- a/cvat/apps/dataset_manager/tests/test_formats.py +++ b/cvat/apps/dataset_manager/tests/test_formats.py @@ -259,7 +259,8 @@ def _generate_task(self, images, **overrides): "name": "parked", "mutable": True, "input_type": "checkbox", - "default_value": False + "default_value": "false", + "values": [], }, ] }, @@ -561,7 +562,8 @@ def _generate_task(self, images): "name": "parked", "mutable": True, "input_type": "checkbox", - "default_value": False + "default_value": "false", + "values": [], }, ] }, @@ -723,7 +725,8 @@ def _generate_task(self, images, annotation_format, **overrides): "name": "parked", "mutable": True, "input_type": "checkbox", - "default_value": False + "default_value": "false", + "values": [], } ] }, diff --git a/cvat/apps/engine/backup.py b/cvat/apps/engine/backup.py index ee994bc81907..f33c789b074e 100644 --- a/cvat/apps/engine/backup.py +++ b/cvat/apps/engine/backup.py @@ -26,7 +26,6 @@ from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.exceptions import ValidationError, PermissionDenied, NotFound -from django_sendfile import sendfile from distutils.util import strtobool import cvat.apps.dataset_manager as dm @@ -37,7 +36,9 @@ LabeledDataSerializer, SegmentSerializer, SimpleJobSerializer, TaskReadSerializer, ProjectReadSerializer, ProjectFileSerializer, TaskFileSerializer, RqIdSerializer) from cvat.apps.engine.utils import ( - av_scan_paths, process_failed_job, configure_dependent_job, get_rq_job_meta, get_import_rq_id, import_resource_with_clean_up_after + av_scan_paths, process_failed_job, configure_dependent_job, + get_rq_job_meta, get_import_rq_id, import_resource_with_clean_up_after, + sendfile ) from cvat.apps.engine.models import ( StorageChoice, StorageMethodChoice, DataChoice, Task, Project, Location) @@ -364,7 +365,7 @@ def _write_data(self, zip_object, target_dir=None): target_dir=target_data_dir, ) else: - raise NotImplementedError() + raise NotImplementedError("We don't currently support backing up tasks with data from cloud storage") def _write_task(self, zip_object, target_dir=None): task_dir = self._db_task.get_dirname() diff --git a/cvat/apps/engine/migrations/0073_alter_attributespec_default_value_and_more.py b/cvat/apps/engine/migrations/0073_alter_attributespec_default_value_and_more.py new file mode 100644 index 000000000000..7bfe361c875b --- /dev/null +++ b/cvat/apps/engine/migrations/0073_alter_attributespec_default_value_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.1 on 2023-07-10 15:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("engine", "0072_alter_issue_updated_date"), + ] + + operations = [ + migrations.AlterField( + model_name="attributespec", + name="default_value", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AlterField( + model_name="attributespec", + name="values", + field=models.CharField(blank=True, max_length=4096), + ), + ] diff --git a/cvat/apps/engine/migrations/0074_alter_labeledimage_source_alter_labeledshape_source_and_more.py b/cvat/apps/engine/migrations/0074_alter_labeledimage_source_alter_labeledshape_source_and_more.py new file mode 100644 index 000000000000..d4505ade89ab --- /dev/null +++ b/cvat/apps/engine/migrations/0074_alter_labeledimage_source_alter_labeledshape_source_and_more.py @@ -0,0 +1,57 @@ +# Generated by Django 4.2.1 on 2023-07-18 07:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("engine", "0073_alter_attributespec_default_value_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="labeledimage", + name="source", + field=models.CharField( + choices=[ + ("auto", "AUTO"), + ("semi-auto", "SEMI_AUTO"), + ("manual", "MANUAL"), + ("file", "FILE"), + ], + default="manual", + max_length=16, + null=True, + ), + ), + migrations.AlterField( + model_name="labeledshape", + name="source", + field=models.CharField( + choices=[ + ("auto", "AUTO"), + ("semi-auto", "SEMI_AUTO"), + ("manual", "MANUAL"), + ("file", "FILE"), + ], + default="manual", + max_length=16, + null=True, + ), + ), + migrations.AlterField( + model_name="labeledtrack", + name="source", + field=models.CharField( + choices=[ + ("auto", "AUTO"), + ("semi-auto", "SEMI_AUTO"), + ("manual", "MANUAL"), + ("file", "FILE"), + ], + default="manual", + max_length=16, + null=True, + ), + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 5e9f6781eb2c..7386aafaa0dc 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -881,8 +881,8 @@ class AttributeSpec(models.Model): mutable = models.BooleanField() input_type = models.CharField(max_length=16, choices=AttributeType.choices()) - default_value = models.CharField(max_length=128) - values = models.CharField(max_length=4096) + default_value = models.CharField(blank=True, max_length=128) + values = models.CharField(blank=True, max_length=4096) class Meta: default_permissions = () @@ -922,6 +922,7 @@ class SourceType(str, Enum): AUTO = 'auto' SEMI_AUTO = 'semi-auto' MANUAL = 'manual' + FILE = 'file' @classmethod def choices(cls): diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index a92df38a0a22..52987755f25d 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -212,30 +212,22 @@ class Meta: 'last_login': { 'allow_null': True } } +class DelimitedStringListField(serializers.ListField): + def to_representation(self, value): + return super().to_representation(value.split('\n')) + + def to_internal_value(self, data): + return '\n'.join(super().to_internal_value(data)) + class AttributeSerializer(serializers.ModelSerializer): - values = serializers.ListField(allow_empty=True, - child=serializers.CharField(max_length=200), + values = DelimitedStringListField(allow_empty=True, + child=serializers.CharField(allow_blank=True, max_length=200), ) class Meta: model = models.AttributeSpec fields = ('id', 'name', 'mutable', 'input_type', 'default_value', 'values') - # pylint: disable=no-self-use - def to_internal_value(self, data): - attribute = data.copy() - attribute['values'] = '\n'.join(data.get('values', [])) - return attribute - - def to_representation(self, instance): - if instance: - rep = super().to_representation(instance) - rep['values'] = instance.values.split('\n') - else: - rep = instance - - return rep - class SublabelSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) attributes = AttributeSerializer(many=True, source='attributespec_set', default=[], diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py index 5663e47e4a71..4cb37551c555 100644 --- a/cvat/apps/engine/tests/test_rest_api.py +++ b/cvat/apps/engine/tests/test_rest_api.py @@ -1811,7 +1811,8 @@ def _create_project(project_data): "name": "bool_attribute", "mutable": True, "input_type": AttributeType.CHECKBOX, - "default_value": "true" + "default_value": "true", + "values": [], }], }, { "name": "person", @@ -2572,7 +2573,8 @@ def test_api_v2_tasks_admin(self): "name": "my_attribute", "mutable": True, "input_type": AttributeType.CHECKBOX, - "default_value": "true" + "default_value": "true", + "values": [], }] }] } @@ -2895,7 +2897,8 @@ def _create_task(task_data, media_data): "name": "bool_attribute", "mutable": True, "input_type": AttributeType.CHECKBOX, - "default_value": "true" + "default_value": "true", + "values": [], }], }, { "name": "person", @@ -2915,7 +2918,8 @@ def _create_task(task_data, media_data): "name": "bool_attribute", "mutable": True, "input_type": AttributeType.CHECKBOX, - "default_value": "true" + "default_value": "true", + "values": [], }], }, { "name": "person", @@ -4649,7 +4653,8 @@ def _create_task(self, owner, assignee, annotation_format=""): "name": "parked", "mutable": True, "input_type": "checkbox", - "default_value": "false" + "default_value": "false", + "values": [], }, ] }, diff --git a/cvat/apps/engine/utils.py b/cvat/apps/engine/utils.py index 63953fe58aca..adeb3bbc07b9 100644 --- a/cvat/apps/engine/utils.py +++ b/cvat/apps/engine/utils.py @@ -14,6 +14,7 @@ import subprocess import os import urllib.parse +import re import logging import platform @@ -31,6 +32,7 @@ from multiprocessing import cpu_count from django.core.exceptions import ValidationError +from django_sendfile import sendfile as _sendfile Import = namedtuple("Import", ["module", "name", "alias"]) @@ -284,3 +286,46 @@ def get_cpu_number() -> int: # the number of cpu cannot be determined cpu_number = 1 return cpu_number + +def make_attachment_file_name(filename: str) -> str: + # Borrowed from sendfile() to minimize changes for users. + # Added whitespace conversion and squashing into a single space + # Added removal of control characters + + filename = str(filename).replace("\\", "\\\\").replace('"', r"\"") + filename = re.sub(r"\s+", " ", filename) + + # From https://github.com/encode/uvicorn/blob/cd18c3b14aa810a4a6ebb264b9a297d6f8afb9ac/uvicorn/protocols/http/httptools_impl.py#L51 + filename = re.sub(r"[\x00-\x1F\x7F]", "", filename) + + return filename + +def sendfile( + request, filename, + attachment=False, attachment_filename=None, mimetype=None, encoding=None +): + """ + Create a response to send file using backend configured in ``SENDFILE_BACKEND`` + + ``filename`` is the absolute path to the file to send. + + If ``attachment`` is ``True`` the ``Content-Disposition`` header will be set accordingly. + This will typically prompt the user to download the file, rather + than view it. But even if ``False``, the user may still be prompted, depending + on the browser capabilities and configuration. + + The ``Content-Disposition`` filename depends on the value of ``attachment_filename``: + + ``None`` (default): Same as ``filename`` + ``False``: No ``Content-Disposition`` filename + ``String``: Value used as filename + + If neither ``mimetype`` or ``encoding`` are specified, then they will be guessed via the + filename (using the standard Python mimetypes module) + """ + # A drop-in replacement for sendfile with extra filename cleaning + + if attachment_filename: + attachment_filename = make_attachment_file_name(attachment_filename) + + return _sendfile(request, filename, attachment, attachment_filename, mimetype, encoding) diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 225fd729cd27..a89ebb773a13 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -42,7 +42,6 @@ from rest_framework.permissions import SAFE_METHODS from rest_framework.response import Response from rest_framework.settings import api_settings -from django_sendfile import sendfile import cvat.apps.dataset_manager as dm import cvat.apps.dataset_manager.views # pylint: disable=unused-import @@ -75,7 +74,7 @@ from cvat.apps.engine.utils import ( av_scan_paths, process_failed_job, configure_dependent_job, parse_exception_message, get_rq_job_meta, get_import_rq_id, - import_resource_with_clean_up_after + import_resource_with_clean_up_after, sendfile ) from cvat.apps.engine import backup from cvat.apps.engine.mixins import PartialUpdateModelMixin, UploadMixin, AnnotationMixin, SerializeMixin @@ -88,7 +87,6 @@ TaskPermission, UserPermission) from cvat.apps.iam.filters import ORGANIZATION_OPEN_API_PARAMETERS from cvat.apps.engine.cache import MediaCache -from cvat.apps.events.handlers import handle_annotations_patch from cvat.apps.engine.view_utils import tus_chunk_action @@ -195,13 +193,13 @@ def annotation_formats(request): }) @action(detail=False, methods=['GET'], url_path='plugins', serializer_class=PluginsSerializer) def plugins(request): - response = { + data = { 'GIT_INTEGRATION': apps.is_installed('cvat.apps.dataset_repo'), 'ANALYTICS': strtobool(os.environ.get("CVAT_ANALYTICS", '0')), 'MODELS': strtobool(os.environ.get("CVAT_SERVERLESS", '0')), 'PREDICT': False, # FIXME: it is unused anymore (for UI only) } - return Response(response) + return Response(PluginsSerializer(data).data) @extend_schema(tags=['projects']) @extend_schema_view( @@ -1801,7 +1799,6 @@ def annotations(self, request, pk): data = dm.task.patch_job_data(pk, serializer.data, action) except (AttributeError, IntegrityError) as e: return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST) - handle_annotations_patch(instance=self._object, annotations=data, action=action) return Response(data) diff --git a/cvat/apps/events/export.py b/cvat/apps/events/export.py index 90aca64db8be..07adc4685702 100644 --- a/cvat/apps/events/export.py +++ b/cvat/apps/events/export.py @@ -15,10 +15,10 @@ from rest_framework import serializers, status from rest_framework.response import Response -from django_sendfile import sendfile from cvat.apps.dataset_manager.views import clear_export_cache, log_exception from cvat.apps.engine.log import slogger +from cvat.apps.engine.utils import sendfile DEFAULT_CACHE_TTL = timedelta(hours=1) diff --git a/cvat/apps/events/handlers.py b/cvat/apps/events/handlers.py index bdfc460d809c..8cabd0938da7 100644 --- a/cvat/apps/events/handlers.py +++ b/cvat/apps/events/handlers.py @@ -402,7 +402,7 @@ def handle_delete(scope, instance, store_in_deletion_cache=False, **kwargs): vlogger.info(message) -def handle_annotations_patch(instance, annotations, action, **kwargs): +def handle_annotations_change(instance, annotations, action, **kwargs): _annotations = deepcopy(annotations) def filter_shape_data(shape): data = { diff --git a/cvat/apps/iam/permissions.py b/cvat/apps/iam/permissions.py index 2a1893e8b1b9..ad539164ab18 100644 --- a/cvat/apps/iam/permissions.py +++ b/cvat/apps/iam/permissions.py @@ -62,7 +62,7 @@ def get_organization(request, obj): except AttributeError as exc: # Skip initialization of organization for those objects that don't related with organization view = request.parser_context.get('view') - if view and view.basename in ('user', 'function', 'request',): + if view and view.basename in ('user', 'function', 'request', 'server'): return request.iam_context['organization'] raise exc @@ -1260,6 +1260,14 @@ def create_scope_view_data(cls, iam_context, job_id): raise ValidationError(str(ex)) return cls(**iam_context, obj=obj, scope='view:data') + @classmethod + def create_scope_view(cls, iam_context, job_id): + try: + obj = Job.objects.get(id=job_id) + except Job.DoesNotExist as ex: + raise ValidationError(str(ex)) + return cls(**iam_context, obj=obj, scope=__class__.Scopes.VIEW) + def __init__(self, **kwargs): self.task_id = kwargs.pop('task_id', None) super().__init__(**kwargs) @@ -2018,3 +2026,74 @@ def has_object_permission(self, request, view, obj): def is_metadata_request(request, view): return request.method == 'OPTIONS' \ or (request.method == 'POST' and view.action == 'metadata' and len(request.data) == 0) + +class AnalyticsReportPermission(OpenPolicyAgentPermission): + class Scopes(StrEnum): + LIST = 'list' + CREATE = 'create' + + @classmethod + def create(cls, request, view, obj, iam_context): + Scopes = __class__.Scopes + permissions = [] + if view.basename == 'analytics_reports': + scopes = cls.get_scopes(request, view, obj) + for scope in scopes: + self = cls.create_base_perm(request, view, scope, iam_context, obj) + permissions.append(self) + + if view.action == Scopes.LIST: + job_id = request.query_params.get('job_id', None) + task_id = request.query_params.get('task_id', None) + project_id = request.query_params.get('project_id', None) + else: + job_id = request.data.get('job_id', None) + task_id = request.data.get('task_id', None) + project_id = request.data.get('project_id', None) + + if job_id: + try: + job = Job.objects.get(id=job_id) + except Job.DoesNotExist as ex: + raise ValidationError(str(ex)) + + iam_context = get_iam_context(request, job) + perm = JobPermission.create_scope_view(iam_context, int(job_id)) + permissions.append(perm) + + if task_id: + try: + task = Task.objects.get(id=task_id) + except Task.DoesNotExist as ex: + raise ValidationError(str(ex)) + + iam_context = get_iam_context(request, task) + perm = TaskPermission.create_scope_view(request, int(task_id), iam_context) + permissions.append(perm) + + if project_id: + try: + project = Project.objects.get(id=project_id) + except Project.DoesNotExist as ex: + raise ValidationError(str(ex)) + + iam_context = get_iam_context(request, project) + perm = ProjectPermission.create_scope_view(iam_context, int(project_id)) + permissions.append(perm) + + return permissions + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.url = settings.IAM_OPA_DATA_URL + '/analytics_reports/allow' + + @staticmethod + def get_scopes(request, view, obj): + Scopes = __class__.Scopes + return [{ + 'list': Scopes.LIST, + 'create': Scopes.CREATE, + }.get(view.action, None)] + + def get_resource(self): + return None diff --git a/cvat/apps/iam/rules/analytics_reports.rego b/cvat/apps/iam/rules/analytics_reports.rego new file mode 100644 index 000000000000..dd5df55bf8f0 --- /dev/null +++ b/cvat/apps/iam/rules/analytics_reports.rego @@ -0,0 +1,33 @@ +package analytics_reports + +import data.utils +import data.organizations + +# input: { +# "scope": <"list"|"create"> or null, +# "auth": { +# "user": { +# "id": , +# "privilege": <"admin"|"business"|"user"|"worker"> or null +# }, +# "organization": { +# "id": , +# "owner": { +# "id": +# }, +# "user": { +# "role": <"owner"|"maintainer"|"supervisor"|"worker"> or null +# } +# } or null, +# } +# } + +default allow = false +allow { + utils.is_admin +} + +allow { + input.scope == utils.LIST + utils.has_perm(utils.WORKER) +} diff --git a/cvat/apps/lambda_manager/tests/assets/tasks.json b/cvat/apps/lambda_manager/tests/assets/tasks.json index 019716f38961..7ae07a1c0d0a 100644 --- a/cvat/apps/lambda_manager/tests/assets/tasks.json +++ b/cvat/apps/lambda_manager/tests/assets/tasks.json @@ -18,7 +18,8 @@ "name": "parked", "mutable": true, "input_type": "checkbox", - "default_value": false + "default_value": "false", + "values": [] } ] }, @@ -47,7 +48,8 @@ "name": "parked", "mutable": true, "input_type": "checkbox", - "default_value": false + "default_value": "false", + "values": [] } ] }, diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index 5f154366bff9..512977792b51 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -10,7 +10,6 @@ import os import textwrap import glob -from django_sendfile import sendfile from copy import deepcopy from enum import Enum from functools import wraps @@ -39,6 +38,7 @@ from cvat.apps.lambda_manager.serializers import ( FunctionCallRequestSerializer, FunctionCallSerializer ) +from cvat.apps.engine.utils import sendfile from cvat.utils.http import make_requests_session from cvat.apps.iam.permissions import LambdaPermission from cvat.apps.iam.filters import ORGANIZATION_OPEN_API_PARAMETERS @@ -210,119 +210,123 @@ def invoke( is_interactive: Optional[bool] = False, request: Optional[Request] = None ): - try: - if db_job is not None and db_job.get_task_id() != db_task.id: - raise ValidationError("Job task id does not match task id", - code=status.HTTP_400_BAD_REQUEST - ) - - payload = {} - data = {k: v for k,v in data.items() if v is not None} - threshold = data.get("threshold") - if threshold: - payload.update({ "threshold": threshold }) - quality = data.get("quality") - mapping = data.get("mapping", {}) - - task_attributes = {} - mapping_by_default = {} - for db_label in (db_task.project.label_set if db_task.project_id else db_task.label_set).prefetch_related("attributespec_set").all(): - mapping_by_default[db_label.name] = { - 'name': db_label.name, - 'attributes': {} + if db_job is not None and db_job.get_task_id() != db_task.id: + raise ValidationError("Job task id does not match task id", + code=status.HTTP_400_BAD_REQUEST + ) + + payload = {} + data = {k: v for k,v in data.items() if v is not None} + + def mandatory_arg(name: str) -> Any: + try: + return data[name] + except KeyError: + raise ValidationError( + "`{}` lambda function was called without mandatory argument: {}" + .format(self.id, name), + code=status.HTTP_400_BAD_REQUEST) + + threshold = data.get("threshold") + if threshold: + payload.update({ "threshold": threshold }) + quality = data.get("quality") + mapping = data.get("mapping", {}) + + task_attributes = {} + mapping_by_default = {} + for db_label in (db_task.project.label_set if db_task.project_id else db_task.label_set).prefetch_related("attributespec_set").all(): + mapping_by_default[db_label.name] = { + 'name': db_label.name, + 'attributes': {} + } + task_attributes[db_label.name] = {} + for attribute in db_label.attributespec_set.all(): + task_attributes[db_label.name][attribute.name] = { + 'input_type': attribute.input_type, + 'values': attribute.values.split('\n') } - task_attributes[db_label.name] = {} - for attribute in db_label.attributespec_set.all(): - task_attributes[db_label.name][attribute.name] = { - 'input_type': attribute.input_type, - 'values': attribute.values.split('\n') - } - if not mapping: - # use mapping by default to avoid labels in mapping which - # don't exist in the task - mapping = mapping_by_default - else: - # filter labels in mapping which don't exist in the task - mapping = {k:v for k,v in mapping.items() if v['name'] in mapping_by_default} + if not mapping: + # use mapping by default to avoid labels in mapping which + # don't exist in the task + mapping = mapping_by_default + else: + # filter labels in mapping which don't exist in the task + mapping = {k:v for k,v in mapping.items() if v['name'] in mapping_by_default} - attr_mapping = { label: mapping[label]['attributes'] if 'attributes' in mapping[label] else {} for label in mapping } - mapping = { modelLabel: mapping[modelLabel]['name'] for modelLabel in mapping } + attr_mapping = { label: mapping[label]['attributes'] if 'attributes' in mapping[label] else {} for label in mapping } + mapping = { modelLabel: mapping[modelLabel]['name'] for modelLabel in mapping } - supported_attrs = {} - for func_label, func_attrs in self.func_attributes.items(): - if func_label not in mapping: - continue + supported_attrs = {} + for func_label, func_attrs in self.func_attributes.items(): + if func_label not in mapping: + continue - mapped_label = mapping[func_label] - mapped_attributes = attr_mapping.get(func_label, {}) - supported_attrs[func_label] = {} + mapped_label = mapping[func_label] + mapped_attributes = attr_mapping.get(func_label, {}) + supported_attrs[func_label] = {} - if mapped_attributes: - task_attr_names = [task_attr for task_attr in task_attributes[mapped_label]] - for attr in func_attrs: - mapped_attr = mapped_attributes.get(attr["name"]) - if mapped_attr in task_attr_names: - supported_attrs[func_label].update({ attr["name"]: task_attributes[mapped_label][mapped_attr] }) + if mapped_attributes: + task_attr_names = [task_attr for task_attr in task_attributes[mapped_label]] + for attr in func_attrs: + mapped_attr = mapped_attributes.get(attr["name"]) + if mapped_attr in task_attr_names: + supported_attrs[func_label].update({ attr["name"]: task_attributes[mapped_label][mapped_attr] }) - # Check job frame boundaries - if db_job: - task_data = db_task.data - data_start_frame = task_data.start_frame - step = task_data.get_frame_step() - - for key, desc in ( - ('frame', 'frame'), - ('frame0', 'start frame'), - ('frame1', 'end frame'), - ): - if key not in data: - continue + # Check job frame boundaries + if db_job: + task_data = db_task.data + data_start_frame = task_data.start_frame + step = task_data.get_frame_step() + + for key, desc in ( + ('frame', 'frame'), + ('frame0', 'start frame'), + ('frame1', 'end frame'), + ): + if key not in data: + continue - abs_frame_id = data_start_frame + data[key] * step - if not db_job.segment.contains_frame(abs_frame_id): - raise ValidationError(f"The {desc} is outside the job range", - code=status.HTTP_400_BAD_REQUEST) + abs_frame_id = data_start_frame + data[key] * step + if not db_job.segment.contains_frame(abs_frame_id): + raise ValidationError(f"The {desc} is outside the job range", + code=status.HTTP_400_BAD_REQUEST) - if self.kind == LambdaType.DETECTOR: - payload.update({ - "image": self._get_image(db_task, data["frame"], quality) - }) - elif self.kind == LambdaType.INTERACTOR: - payload.update({ - "image": self._get_image(db_task, data["frame"], quality), - "pos_points": data["pos_points"][2:] if self.startswith_box else data["pos_points"], - "neg_points": data["neg_points"], - "obj_bbox": data["pos_points"][0:2] if self.startswith_box else None - }) - elif self.kind == LambdaType.REID: - payload.update({ - "image0": self._get_image(db_task, data["frame0"], quality), - "image1": self._get_image(db_task, data["frame1"], quality), - "boxes0": data["boxes0"], - "boxes1": data["boxes1"] - }) - max_distance = data.get("max_distance") - if max_distance: - payload.update({ - "max_distance": max_distance - }) - elif self.kind == LambdaType.TRACKER: + if self.kind == LambdaType.DETECTOR: + payload.update({ + "image": self._get_image(db_task, mandatory_arg("frame"), quality) + }) + elif self.kind == LambdaType.INTERACTOR: + payload.update({ + "image": self._get_image(db_task, mandatory_arg("frame"), quality), + "pos_points": mandatory_arg("pos_points")[2:] if self.startswith_box else mandatory_arg("pos_points"), + "neg_points": mandatory_arg("neg_points"), + "obj_bbox": mandatory_arg("pos_points")[0:2] if self.startswith_box else None + }) + elif self.kind == LambdaType.REID: + payload.update({ + "image0": self._get_image(db_task, mandatory_arg("frame0"), quality), + "image1": self._get_image(db_task, mandatory_arg("frame1"), quality), + "boxes0": mandatory_arg("boxes0"), + "boxes1": mandatory_arg("boxes1") + }) + max_distance = data.get("max_distance") + if max_distance: payload.update({ - "image": self._get_image(db_task, data["frame"], quality), - "shapes": data.get("shapes", []), - "states": data.get("states", []) + "max_distance": max_distance }) - else: - raise ValidationError( - '`{}` lambda function has incorrect type: {}' - .format(self.id, self.kind), - code=status.HTTP_500_INTERNAL_SERVER_ERROR) - except KeyError as err: + elif self.kind == LambdaType.TRACKER: + payload.update({ + "image": self._get_image(db_task, mandatory_arg("frame"), quality), + "shapes": data.get("shapes", []), + "states": data.get("states", []) + }) + else: raise ValidationError( - "`{}` lambda function was called without mandatory argument: {}" - .format(self.id, str(err)), - code=status.HTTP_400_BAD_REQUEST) + '`{}` lambda function has incorrect type: {}' + .format(self.id, self.kind), + code=status.HTTP_500_INTERNAL_SERVER_ERROR) if is_interactive and request: interactive_function_call_signal.send(sender=self, request=request) diff --git a/cvat/apps/opencv/views.py b/cvat/apps/opencv/views.py index 3090ad684fb3..40a0d06d8143 100644 --- a/cvat/apps/opencv/views.py +++ b/cvat/apps/opencv/views.py @@ -1,7 +1,7 @@ import os import glob from django.conf import settings -from django_sendfile import sendfile +from cvat.apps.engine.utils import sendfile def OpenCVLibrary(request): dirname = os.path.join(settings.STATIC_ROOT, 'opencv', 'js') diff --git a/cvat/apps/quality_control/quality_reports.py b/cvat/apps/quality_control/quality_reports.py index 32c9f72205e6..1f3ff5682569 100644 --- a/cvat/apps/quality_control/quality_reports.py +++ b/cvat/apps/quality_control/quality_reports.py @@ -1755,6 +1755,10 @@ def _find_closest_unmatched_shape(shape: dm.Annotation): _matched_shapes.add(matched_ann_id) resulting_distances.append(similarity) + resulting_distances = [ + sim if sim is not None and (sim >= 0) else 0 for sim in resulting_distances + ] + mean_iou = np.mean(resulting_distances) if resulting_distances else 0 if ( diff --git a/cvat/rqworker.py b/cvat/rqworker.py index 78319ff2e841..d368a1ef2629 100644 --- a/cvat/rqworker.py +++ b/cvat/rqworker.py @@ -68,11 +68,13 @@ def execute_job(self, *args, **kwargs): if os.environ.get("COVERAGE_PROCESS_START"): import coverage + default_exit = os._exit - def coverage_exit(): + def coverage_exit(*args, **kwargs): cov = coverage.Coverage.current() - cov.stop() - cov.save() - os._exit(0) + if cov: + cov.stop() + cov.save() + default_exit(*args, **kwargs) os._exit = coverage_exit diff --git a/cvat/schema.yml b/cvat/schema.yml index 22829eac94b1..091b33daa052 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: CVAT REST API - version: 2.5.1 + version: 2.5.2 description: REST API for Computer Vision Annotation Tool (CVAT) termsOfService: https://www.google.com/policies/terms/ contact: @@ -12,6 +12,96 @@ info: name: MIT License url: https://en.wikipedia.org/wiki/MIT_License paths: + /api/analytics/reports: + get: + operationId: analytics_get_reports + description: Receive analytics report + summary: Method returns analytics report + parameters: + - in: query + name: end_date + schema: + type: string + format: date-time + description: Specify the end date for filtering report data. + - in: query + name: job_id + schema: + type: integer + description: Specify job ID + - in: query + name: project_id + schema: + type: integer + description: Specify project ID + - in: query + name: start_date + schema: + type: string + format: date-time + description: Specify a start date for filtering report data. + - in: query + name: task_id + schema: + type: integer + description: Specify task ID + tags: + - analytics + security: + - sessionAuth: [] + csrfAuth: [] + tokenAuth: [] + - signatureAuth: [] + - basicAuth: [] + responses: + '200': + content: + application/vnd.cvat+json: + schema: + $ref: '#/components/schemas/AnalyticsReport' + description: '' + '404': + description: Not found + post: + operationId: analytics_create_report + summary: Creates a analytics report asynchronously and allows to check request + status + parameters: + - in: query + name: rq_id + schema: + type: string + description: | + The report creation request id. Can be specified to check the report + creation status. + tags: + - analytics + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AnalyticsReportCreateRequest' + security: + - sessionAuth: [] + csrfAuth: [] + tokenAuth: [] + - signatureAuth: [] + - basicAuth: [] + responses: + '201': + description: A analytics report request has been computed + '202': + content: + application/vnd.cvat+json: + schema: + $ref: '#/components/schemas/RqId' + description: | + A analytics report request has been enqueued, the request id is returned. + The request status can be checked at this endpoint by passing the rq_id + as the query parameter. If the request id is specified, this response + means the analytics report request is queued or is being processed. + '400': + description: Invalid or failed request, check the response data for details /api/assets: post: operationId: assets_create @@ -5826,6 +5916,47 @@ components: - description - name - version + AnalyticsReport: + type: object + properties: + created_date: + type: string + format: date-time + target: + $ref: '#/components/schemas/AnalyticsReportTargetEnum' + job_id: + type: integer + task_id: + type: integer + project_id: + type: integer + statistics: + type: array + items: + $ref: '#/components/schemas/Metric' + required: + - created_date + - statistics + - target + AnalyticsReportCreateRequest: + type: object + properties: + job_id: + type: integer + task_id: + type: integer + project_id: + type: integer + AnalyticsReportTargetEnum: + enum: + - job + - task + - project + type: string + description: |- + * `job` - JOB + * `task` - TASK + * `project` - PROJECT AnnotationConflict: type: object properties: @@ -5993,7 +6124,6 @@ components: type: string maxLength: 200 required: - - default_value - input_type - mutable - name @@ -6011,16 +6141,13 @@ components: $ref: '#/components/schemas/InputTypeEnum' default_value: type: string - minLength: 1 maxLength: 128 values: type: array items: type: string - minLength: 1 maxLength: 200 required: - - default_value - input_type - mutable - name @@ -6093,6 +6220,23 @@ components: maxLength: 150 required: - username + BinaryOperation: + type: object + properties: + left: + type: string + nullable: true + description: The name of the data series used as the left (first) operand + of the binary operation. + operator: + $ref: '#/components/schemas/OperatorEnum' + right: + type: string + nullable: true + description: The name of the data series used as the right (second) operand + of the binary operation. + required: + - operator ChunkType: enum: - video @@ -6300,6 +6444,18 @@ components: * `KEY_FILE_PATH` - KEY_FILE_PATH * `ANONYMOUS_ACCESS` - ANONYMOUS_ACCESS * `CONNECTION_STRING` - CONNECTION_STRING + DataFrame: + type: object + properties: + value: + type: number + format: double + date: + type: string + format: date + required: + - date + - value DataMetaRead: type: object properties: @@ -6563,6 +6719,14 @@ components: oneOf: - $ref: '#/components/schemas/DatasetFileRequest' nullable: true + DefaultViewEnum: + enum: + - numeric + - histogram + type: string + description: |- + * `numeric` - NUMERIC + * `histogram` - HISTOGRAM Event: type: object properties: @@ -6895,6 +7059,16 @@ components: required: - function - task + GranularityEnum: + enum: + - day + - week + - month + type: string + description: |- + * `day` - DAY + * `week` - WEEK + * `month` - MONTH InputTypeEnum: enum: - checkbox @@ -7600,6 +7774,39 @@ components: anyOf: - $ref: '#/components/schemas/User' - $ref: '#/components/schemas/BasicUser' + Metric: + type: object + properties: + name: + type: string + title: + type: string + description: + type: string + granularity: + nullable: true + oneOf: + - $ref: '#/components/schemas/GranularityEnum' + - $ref: '#/components/schemas/NullEnum' + default_view: + $ref: '#/components/schemas/DefaultViewEnum' + data_series: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/DataFrame' + transformations: + type: array + items: + $ref: '#/components/schemas/Transformation' + required: + - data_series + - default_view + - description + - name + - title + - transformations NullEnum: enum: - null @@ -7622,6 +7829,18 @@ components: * `in progress` - IN_PROGRESS * `completed` - COMPLETED * `rejected` - REJECTED + OperatorEnum: + enum: + - + + - '-' + - '*' + - / + type: string + description: |- + * `+` - ADDITION + * `-` - SUBTRACTION + * `*` - MULTIPLICATION + * `/` - DIVISION OrganizationRead: type: object properties: @@ -9412,6 +9631,17 @@ components: required: - frame - type + Transformation: + type: object + properties: + name: + type: string + binary: + allOf: + - $ref: '#/components/schemas/BinaryOperation' + nullable: true + required: + - name User: type: object properties: diff --git a/cvat/settings/base.py b/cvat/settings/base.py index a78cc9cb7c08..82b198d36426 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -142,6 +142,7 @@ def add_ssh_keys(): 'cvat.apps.health', 'cvat.apps.events', 'cvat.apps.quality_control', + 'cvat.apps.analytics_report', ] SITE_ID = 1 @@ -295,6 +296,7 @@ class CVAT_QUEUES(Enum): WEBHOOKS = 'webhooks' NOTIFICATIONS = 'notifications' QUALITY_REPORTS = 'quality_reports' + ANALYTICS_REPORTS = 'analytics_reports' CLEANING = 'cleaning' RQ_QUEUES = { @@ -334,6 +336,12 @@ class CVAT_QUEUES(Enum): 'DB': 0, 'DEFAULT_TIMEOUT': '1h', }, + CVAT_QUEUES.ANALYTICS_REPORTS.value: { + 'HOST': 'localhost', + 'PORT': 6379, + 'DB': 0, + 'DEFAULT_TIMEOUT': '1h' + }, CVAT_QUEUES.CLEANING.value: { 'HOST': 'localhost', 'PORT': 6379, diff --git a/cvat/settings/development.py b/cvat/settings/development.py index 2898fc88628f..b6c02a83522f 100644 --- a/cvat/settings/development.py +++ b/cvat/settings/development.py @@ -65,5 +65,6 @@ DATABASES['default']['HOST'] = os.getenv('CVAT_POSTGRES_HOST', 'localhost') QUALITY_CHECK_JOB_DELAY = 5 +ANALYTICS_CHECK_JOB_DELAY = 15 SMOKESCREEN_ENABLED = False diff --git a/cvat/settings/testing.py b/cvat/settings/testing.py index 3fe04f1ef3af..74703f86ad8a 100644 --- a/cvat/settings/testing.py +++ b/cvat/settings/testing.py @@ -95,3 +95,7 @@ def __init__(self, *args, **kwargs): config["ASYNC"] = False super().__init__(*args, **kwargs) + +# No need to profile unit tests +INSTALLED_APPS.remove('silk') +MIDDLEWARE.remove('silk.middleware.SilkyMiddleware') diff --git a/cvat/settings/testing_rest.py b/cvat/settings/testing_rest.py index 9351f35bd5e4..89aa1d71a163 100644 --- a/cvat/settings/testing_rest.py +++ b/cvat/settings/testing_rest.py @@ -15,5 +15,12 @@ # Note that DB initialization triggers server signals, # so quality report updates are scheduled for applicable jobs. QUALITY_CHECK_JOB_DELAY = 10000 +ANALYTICS_CHECK_JOB_DELAY = 10000 IMPORT_CACHE_CLEAN_DELAY = timedelta(seconds=30) + +# The tests should not fail due to high disk utilization of CI infrastructure that we have no control over +# But let's keep this check enabled +HEALTH_CHECK = { + 'DISK_USAGE_MAX': 100, # percent +} diff --git a/cvat/urls.py b/cvat/urls.py index df95a399c3f8..9f34b83dfe04 100644 --- a/cvat/urls.py +++ b/cvat/urls.py @@ -54,3 +54,6 @@ if apps.is_installed('health_check'): urlpatterns.append(path('api/server/health/', include('health_check.urls'))) + +if apps.is_installed('cvat.apps.analytics_report'): + urlpatterns.append(path('api/', include('cvat.apps.analytics_report.urls'))) diff --git a/dev/format_python_code.sh b/dev/format_python_code.sh index e31a712011c4..a67bf08572e7 100755 --- a/dev/format_python_code.sh +++ b/dev/format_python_code.sh @@ -17,7 +17,13 @@ fi # The commands must be run on each module directory separately, # otherwise tools confuse the "current" module -for paths in "cvat-sdk" "cvat-cli" "tests/python/" "cvat/apps/quality_control"; do +for paths in \ + "cvat-sdk" \ + "cvat-cli" \ + "tests/python/" \ + "cvat/apps/quality_control" \ + "cvat/apps/analytics_report" \ + ; do ${BLACK} -- ${paths} ${ISORT} -- ${paths} done diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 404d59d3633f..d6da2998eace 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -66,6 +66,19 @@ services: ports: - '9094:9094' + cvat_worker_analytics_reports: + environment: + CVAT_ANALYTICS_CHECK_JOB_DELAY: 5 + # For debugging, make sure to set 1 process + # Due to the supervisord specifics, the extra processes will fail and + # after few attempts supervisord will give up restarting, leaving only 1 process + NUMPROCS: 1 + CVAT_DEBUG_ENABLED: '${CVAT_DEBUG_ENABLED:-no}' + CVAT_DEBUG_PORT: '9095' + COVERAGE_PROCESS_START: + ports: + - '9095:9095' + cvat_worker_annotation: environment: # For debugging, make sure to set 1 process diff --git a/docker-compose.yml b/docker-compose.yml index 39b3c7e247b5..0bf34777f604 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,7 +25,7 @@ services: cvat_server: container_name: cvat_server - image: cvat/server:${CVAT_VERSION:-v2.5.1} + image: cvat/server:${CVAT_VERSION:-v2.5.2} restart: always depends_on: - cvat_redis @@ -64,7 +64,7 @@ services: cvat_utils: container_name: cvat_utils - image: cvat/server:${CVAT_VERSION:-v2.5.1} + image: cvat/server:${CVAT_VERSION:-v2.5.2} restart: always depends_on: - cvat_redis @@ -89,7 +89,7 @@ services: cvat_worker_import: container_name: cvat_worker_import - image: cvat/server:${CVAT_VERSION:-v2.5.1} + image: cvat/server:${CVAT_VERSION:-v2.5.2} restart: always depends_on: - cvat_redis @@ -112,7 +112,7 @@ services: cvat_worker_export: container_name: cvat_worker_export - image: cvat/server:${CVAT_VERSION:-v2.5.1} + image: cvat/server:${CVAT_VERSION:-v2.5.2} restart: always depends_on: - cvat_redis @@ -135,7 +135,7 @@ services: cvat_worker_annotation: container_name: cvat_worker_annotation - image: cvat/server:${CVAT_VERSION:-v2.5.1} + image: cvat/server:${CVAT_VERSION:-v2.5.2} restart: always depends_on: - cvat_redis @@ -158,7 +158,7 @@ services: cvat_worker_webhooks: container_name: cvat_worker_webhooks - image: cvat/server:${CVAT_VERSION:-v2.5.1} + image: cvat/server:${CVAT_VERSION:-v2.5.2} restart: always depends_on: - cvat_redis @@ -182,7 +182,7 @@ services: cvat_worker_quality_reports: container_name: cvat_worker_quality_reports - image: cvat/server:${CVAT_VERSION:-v2.5.1} + image: cvat/server:${CVAT_VERSION:-v2.5.2} restart: always depends_on: - cvat_redis @@ -202,9 +202,32 @@ services: networks: - cvat + cvat_worker_analytics_reports: + container_name: cvat_worker_analytics_reports + image: cvat/server:${CVAT_VERSION:-v2.5.2} + restart: always + depends_on: + - cvat_redis + - cvat_db + environment: + CVAT_REDIS_HOST: 'cvat_redis' + CVAT_POSTGRES_HOST: 'cvat_db' + DJANGO_LOG_SERVER_HOST: vector + DJANGO_LOG_SERVER_PORT: 80 + CLICKHOUSE_HOST: clickhouse + no_proxy: clickhouse,grafana,vector,nuclio,opa,${no_proxy:-} + NUMPROCS: 2 + command: -c supervisord/worker.analytics_reports.conf + volumes: + - cvat_data:/home/django/data + - cvat_keys:/home/django/keys + - cvat_logs:/home/django/logs + networks: + - cvat + cvat_ui: container_name: cvat_ui - image: cvat/ui:${CVAT_VERSION:-v2.5.1} + image: cvat/ui:${CVAT_VERSION:-v2.5.2} restart: always depends_on: - cvat_server diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml index 1522abc478fd..405c02ab74e5 100644 --- a/helm-chart/Chart.yaml +++ b/helm-chart/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.8.0 +version: 0.9.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to @@ -58,4 +58,4 @@ dependencies: - name: traefik version: 10.24.0 repository: https://helm.traefik.io/traefik - condition: ingress.enabled + condition: traefik.enabled diff --git a/helm-chart/cvat.values.yaml b/helm-chart/cvat.values.yaml new file mode 100644 index 000000000000..9765325f3cc1 --- /dev/null +++ b/helm-chart/cvat.values.yaml @@ -0,0 +1,53 @@ +analytics: + enabled: true + +ingress: + enabled: true + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web + kubernetes.io/ingress.class: traefik + hosts: + - host: cvat.local + paths: + - path: /api + pathType: "Prefix" + service: + name: backend-service + port: 8080 + - path: /admin + pathType: "Prefix" + service: + name: backend-service + port: 8080 + - path: /static + pathType: "Prefix" + service: + name: backend-service + port: 8080 + - path: /django-rq + pathType: "Prefix" + service: + name: backend-service + port: 8080 + - path: /git + pathType: "Prefix" + service: + name: backend-service + port: 8080 + - path: /opencv + pathType: "Prefix" + service: + name: backend-service + port: 8080 + - path: /profiler + pathType: "Prefix" + service: + name: backend-service + port: 8080 + - path : / + pathType: "Prefix" + service: + name: frontend-service + port: 80 +traefik: + enabled: true diff --git a/helm-chart/templates/cvat_backend/server/deployment.yml b/helm-chart/templates/cvat_backend/server/deployment.yml index 1d5c25f88982..4c75648d1779 100644 --- a/helm-chart/templates/cvat_backend/server/deployment.yml +++ b/helm-chart/templates/cvat_backend/server/deployment.yml @@ -114,6 +114,10 @@ spec: ports: - containerPort: 8080 volumeMounts: + {{- if not .Values.cvat.backend.disableDistinctCachePerService }} + - mountPath: /home/django/data/cache + name: cvat-server-backend-cache + {{- end }} - mountPath: /home/django/data name: cvat-backend-data subPath: data @@ -140,6 +144,10 @@ spec: {{- end }} volumeMounts: {{- if .Values.cvat.backend.defaultStorage.enabled }} + {{- if not .Values.cvat.backend.disableDistinctCachePerService }} + - mountPath: /home/django/data/cache + name: cvat-server-backend-cache + {{- end }} - mountPath: /home/django/data name: cvat-backend-data subPath: data @@ -170,6 +178,10 @@ spec: - name: cvat-backend-data persistentVolumeClaim: claimName: "{{ .Release.Name }}-backend-data" + {{- if not .Values.cvat.backend.disableDistinctCachePerService }} + - name: cvat-server-backend-cache + emptyDir: {} + {{- end }} {{- end }} {{- with .Values.cvat.backend.server.additionalVolumes }} {{- toYaml . | nindent 8 }} diff --git a/helm-chart/templates/cvat_backend/storage.yml b/helm-chart/templates/cvat_backend/storage.yml index f30b768ca39b..b21e61df4b6d 100644 --- a/helm-chart/templates/cvat_backend/storage.yml +++ b/helm-chart/templates/cvat_backend/storage.yml @@ -10,8 +10,15 @@ metadata: tier: backend spec: accessModes: - - ReadWriteOnce + {{- if .Values.cvat.backend.defaultStorage.accessModes }} + {{ .Values.cvat.backend.defaultStorage.accessModes | toYaml | nindent 4 }} + {{- else }} + - ReadWriteMany + {{- end }} + {{- if .Values.cvat.backend.defaultStorage.storageClassName }} + storageClassName: {{ .Values.cvat.backend.defaultStorage.storageClassName }} + {{- end }} resources: requests: storage: {{ .Values.cvat.backend.defaultStorage.size }} -{{- end}} +{{- end }} diff --git a/helm-chart/templates/cvat_backend/utils/deployment.yml b/helm-chart/templates/cvat_backend/utils/deployment.yml index e57b056c31ba..12c5dcf0c5be 100644 --- a/helm-chart/templates/cvat_backend/utils/deployment.yml +++ b/helm-chart/templates/cvat_backend/utils/deployment.yml @@ -108,6 +108,10 @@ spec: ports: - containerPort: 8080 volumeMounts: + {{- if not .Values.cvat.backend.disableDistinctCachePerService }} + - mountPath: /home/django/data/cache + name: cvat-utils-backend-cache + {{- end }} - mountPath: /home/django/data name: cvat-backend-data subPath: data @@ -134,6 +138,10 @@ spec: {{- end }} volumeMounts: {{- if .Values.cvat.backend.defaultStorage.enabled }} + {{- if not .Values.cvat.backend.disableDistinctCachePerService }} + - mountPath: /home/django/data/cache + name: cvat-utils-backend-cache + {{- end }} - mountPath: /home/django/data name: cvat-backend-data subPath: data @@ -164,6 +172,11 @@ spec: - name: cvat-backend-data persistentVolumeClaim: claimName: "{{ .Release.Name }}-backend-data" + + {{- if not .Values.cvat.backend.disableDistinctCachePerService }} + - name: cvat-utils-backend-cache + emptyDir: {} + {{- end }} {{- end }} {{- with .Values.cvat.backend.utils.additionalVolumes }} {{- toYaml . | nindent 8 }} diff --git a/helm-chart/templates/cvat_backend/worker_analyticsreports/deployment.yml b/helm-chart/templates/cvat_backend/worker_analyticsreports/deployment.yml new file mode 100644 index 000000000000..a34d95dadbc8 --- /dev/null +++ b/helm-chart/templates/cvat_backend/worker_analyticsreports/deployment.yml @@ -0,0 +1,125 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-backend-worker-analyticsreports + namespace: {{ .Release.Namespace }} + labels: + app: cvat-app + tier: backend + component: worker-analyticsreports + {{- include "cvat.labels" . | nindent 4 }} + {{- with .Values.cvat.backend.worker.analyticsreports.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.cvat.backend.worker.analyticsreports.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.cvat.backend.worker.analyticsreports.replicas }} + strategy: + type: Recreate + selector: + matchLabels: + {{- include "cvat.labels" . | nindent 6 }} + {{- with .Values.cvat.backend.worker.analyticsreports.labels }} + {{- toYaml . | nindent 6 }} + {{- end }} + app: cvat-app-worker-analyticsreports + tier: backend + component: worker-analyticsreports + template: + metadata: + labels: + app: cvat-app-worker-analyticsreports + tier: backend + component: worker-analyticsreports + {{- include "cvat.labels" . | nindent 8 }} + {{- with .Values.cvat.backend.worker.analyticsreports.labels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.cvat.backend.worker.analyticsreports.annotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + containers: + - name: cvat-app-backend-worker-analyticsreports-container + image: {{ .Values.cvat.backend.image }}:{{ .Values.cvat.backend.tag }} + imagePullPolicy: {{ .Values.cvat.backend.imagePullPolicy }} + {{- with .Values.cvat.backend.worker.analyticsreports.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + args: ["-c", "supervisord/worker.analytics_reports.conf"] + env: + {{- if .Values.redis.enabled }} + - name: CVAT_REDIS_HOST + value: "{{ .Release.Name }}-redis-master" + {{- else }} + - name: CVAT_REDIS_HOST + value: "{{ .Values.redis.external.host }}" + {{- end }} + - name: CVAT_REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: "{{ tpl (.Values.redis.secret.name) . }}" + key: redis-password + {{- if .Values.postgresql.enabled }} + - name: CVAT_POSTGRES_HOST + value: "{{ .Release.Name }}-postgresql" + - name: CVAT_POSTGRES_PORT + value: "{{ .Values.postgresql.service.ports.postgresql }}" + {{- else }} + - name: CVAT_POSTGRES_HOST + value: "{{ .Values.postgresql.external.host }}" + - name: CVAT_POSTGRES_PORT + value: "{{ .Values.postgresql.external.port }}" + {{- end }} + - name: CVAT_POSTGRES_USER + valueFrom: + secretKeyRef: + name: "{{ tpl (.Values.postgresql.secret.name) . }}" + key: username + - name: CVAT_POSTGRES_DBNAME + valueFrom: + secretKeyRef: + name: "{{ tpl (.Values.postgresql.secret.name) . }}" + key: database + - name: CVAT_POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: "{{ tpl (.Values.postgresql.secret.name) . }}" + key: password + {{ include "cvat.sharedBackendEnv" . | indent 10 }} + {{- if .Values.analytics.enabled}} + - name: DJANGO_LOG_SERVER_HOST + value: "{{ .Release.Name }}-vector" + - name: DJANGO_LOG_SERVER_PORT + value: "80" + - name: CLICKHOUSE_HOST + value: "{{ .Release.Name }}-clickhouse" + {{- end }} + {{- with .Values.cvat.backend.worker.analyticsreports.additionalEnv }} + {{- toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.cvat.backend.worker.analyticsreports.additionalVolumeMounts }} + volumeMounts: + {{- toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.cvat.backend.worker.analyticsreports.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.cvat.backend.worker.analyticsreports.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.cvat.backend.worker.analyticsreports.additionalVolumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm-chart/templates/cvat_backend/worker_annotation/deployment.yml b/helm-chart/templates/cvat_backend/worker_annotation/deployment.yml index 031a06a4610e..7432ff136d88 100644 --- a/helm-chart/templates/cvat_backend/worker_annotation/deployment.yml +++ b/helm-chart/templates/cvat_backend/worker_annotation/deployment.yml @@ -106,6 +106,10 @@ spec: {{- toYaml . | nindent 10 }} {{- end }} volumeMounts: + {{- if not .Values.cvat.backend.disableDistinctCachePerService }} + - mountPath: /home/django/data/cache + name: cvat-worker-annotation-backend-cache + {{- end }} - mountPath: /home/django/data name: cvat-backend-data subPath: data @@ -135,6 +139,10 @@ spec: {{- end }} volumeMounts: {{- if .Values.cvat.backend.defaultStorage.enabled }} + {{- if not .Values.cvat.backend.disableDistinctCachePerService }} + - mountPath: /home/django/data/cache + name: cvat-worker-annotation-backend-cache + {{- end }} - mountPath: /home/django/data name: cvat-backend-data subPath: data @@ -165,6 +173,8 @@ spec: - name: cvat-backend-data persistentVolumeClaim: claimName: "{{ .Release.Name }}-backend-data" + - name: cvat-worker-annotation-backend-cache + emptyDir: {} {{- end }} {{- with .Values.cvat.backend.worker.annotation.additionalVolumes }} {{- toYaml . | nindent 8 }} diff --git a/helm-chart/templates/cvat_backend/worker_export/deployment.yml b/helm-chart/templates/cvat_backend/worker_export/deployment.yml index 2011a3e79d44..4fd70182528a 100644 --- a/helm-chart/templates/cvat_backend/worker_export/deployment.yml +++ b/helm-chart/templates/cvat_backend/worker_export/deployment.yml @@ -106,6 +106,10 @@ spec: {{- toYaml . | nindent 10 }} {{- end }} volumeMounts: + {{- if not .Values.cvat.backend.disableDistinctCachePerService }} + - mountPath: /home/django/data/cache + name: cvat-worker-export-backend-cache + {{- end }} - mountPath: /home/django/data name: cvat-backend-data subPath: data @@ -135,6 +139,10 @@ spec: {{- end }} volumeMounts: {{- if .Values.cvat.backend.defaultStorage.enabled }} + {{- if not .Values.cvat.backend.disableDistinctCachePerService }} + - mountPath: /home/django/data/cache + name: cvat-worker-export-backend-cache + {{- end }} - mountPath: /home/django/data name: cvat-backend-data subPath: data @@ -165,6 +173,11 @@ spec: - name: cvat-backend-data persistentVolumeClaim: claimName: "{{ .Release.Name }}-backend-data" + + {{- if not .Values.cvat.backend.disableDistinctCachePerService }} + - name: cvat-worker-export-backend-cache + emptyDir: {} + {{- end }} {{- end }} {{- with .Values.cvat.backend.worker.export.additionalVolumes }} {{- toYaml . | nindent 8 }} diff --git a/helm-chart/templates/cvat_backend/worker_import/deployment.yml b/helm-chart/templates/cvat_backend/worker_import/deployment.yml index beb55325eef4..11d80051d7da 100644 --- a/helm-chart/templates/cvat_backend/worker_import/deployment.yml +++ b/helm-chart/templates/cvat_backend/worker_import/deployment.yml @@ -106,6 +106,10 @@ spec: {{- toYaml . | nindent 10 }} {{- end }} volumeMounts: + {{- if not .Values.cvat.backend.disableDistinctCachePerService }} + - mountPath: /home/django/data/cache + name: cvat-worker-import-backend-cache + {{- end }} - mountPath: /home/django/data name: cvat-backend-data subPath: data @@ -135,6 +139,10 @@ spec: {{- end }} volumeMounts: {{- if .Values.cvat.backend.defaultStorage.enabled }} + {{- if not .Values.cvat.backend.disableDistinctCachePerService }} + - mountPath: /home/django/data/cache + name: cvat-worker-import-backend-cache + {{- end }} - mountPath: /home/django/data name: cvat-backend-data subPath: data @@ -165,6 +173,8 @@ spec: - name: cvat-backend-data persistentVolumeClaim: claimName: "{{ .Release.Name }}-backend-data" + - name: cvat-worker-import-backend-cache + emptyDir: {} {{- end }} {{- with .Values.cvat.backend.worker.import.additionalVolumes }} {{- toYaml . | nindent 8 }} diff --git a/helm-chart/templates/cvat_backend/worker_qualityreports/deployment.yml b/helm-chart/templates/cvat_backend/worker_qualityreports/deployment.yml index 490e8f8930a5..b7d05254d57c 100644 --- a/helm-chart/templates/cvat_backend/worker_qualityreports/deployment.yml +++ b/helm-chart/templates/cvat_backend/worker_qualityreports/deployment.yml @@ -25,13 +25,13 @@ spec: {{- with .Values.cvat.backend.worker.qualityreports.labels }} {{- toYaml . | nindent 6 }} {{- end }} - app: cvat-app-worker-qualityreports + app: cvat-app tier: backend component: worker-qualityreports template: metadata: labels: - app: cvat-app-worker-qualityreports + app: cvat-app tier: backend component: worker-qualityreports {{- include "cvat.labels" . | nindent 8 }} diff --git a/tests/values.test.yaml b/helm-chart/test.values.yaml similarity index 87% rename from tests/values.test.yaml rename to helm-chart/test.values.yaml index 58c5b66ccf79..8e85e3944166 100644 --- a/tests/values.test.yaml +++ b/helm-chart/test.values.yaml @@ -1,5 +1,9 @@ cvat: backend: + defaultStorage: + accessModes: + - ReadWriteOnce + disableDistinctCachePerService: true server: additionalVolumeMounts: - mountPath: /home/django/share diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index 1c04bb2a2990..aa63541a37da 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -73,6 +73,16 @@ cvat: additionalEnv: [] additionalVolumes: [] additionalVolumeMounts: [] + analyticsreports: + replicas: 1 + labels: {} + annotations: {} + resources: {} + affinity: {} + tolerations: [] + additionalEnv: [] + additionalVolumes: [] + additionalVolumeMounts: [] utils: replicas: 1 labels: {} @@ -85,7 +95,7 @@ cvat: additionalVolumeMounts: [] replicas: 1 image: cvat/server - tag: v2.5.1 + tag: v2.5.2 imagePullPolicy: Always permissionFix: enabled: true @@ -101,11 +111,15 @@ cvat: name: http defaultStorage: enabled: true +# storageClassName: default +# accessModes: +# - ReadWriteMany size: 20Gi + disableDistinctCachePerService: false frontend: replicas: 1 image: cvat/ui - tag: v2.5.1 + tag: v2.5.2 imagePullPolicy: Always labels: {} # test: test @@ -211,8 +225,9 @@ postgresql: postgres_password: cvat_postgresql_postgres replication_password: cvat_postgresql_replica + +# See https://github.com/bitnami/charts/blob/master/bitnami/redis/ for more info redis: - #See https://github.com/bitnami/charts/blob/master/bitnami/redis/ for more info enabled: true external: host: 127.0.0.1 @@ -329,59 +344,63 @@ grafana: enabled: false ingress: - enabled: true - annotations: - traefik.ingress.kubernetes.io/router.entrypoints: web - kubernetes.io/ingress.class: traefik - hosts: - - host: cvat.local - paths: - - path: /api - pathType: "Prefix" - service: - name: backend-service - port: 8080 - - path: /admin - pathType: "Prefix" - service: - name: backend-service - port: 8080 - - path: /static - pathType: "Prefix" - service: - name: backend-service - port: 8080 - - path: /django-rq - pathType: "Prefix" - service: - name: backend-service - port: 8080 - - path: /git - pathType: "Prefix" - service: - name: backend-service - port: 8080 - - path: /opencv - pathType: "Prefix" - service: - name: backend-service - port: 8080 - - path: /profiler - pathType: "Prefix" - service: - name: backend-service - port: 8080 - - path : / - pathType: "Prefix" - service: - name: frontend-service - port: 80 - # tls: - # - hosts: - # - - # secretName: ingress-tls-cvat + enabled: false + +# In case you need an ingress, write them manually in your my.values.eml, see example below: +# enabled: true +# annotations: +# traefik.ingress.kubernetes.io/router.entrypoints: web +# kubernetes.io/ingress.class: traefik +# hosts: +# - host: cvat.local +# paths: +# - path: /api +# pathType: "Prefix" +# service: +# name: backend-service +# port: 8080 +# - path: /admin +# pathType: "Prefix" +# service: +# name: backend-service +# port: 8080 +# - path: /static +# pathType: "Prefix" +# service: +# name: backend-service +# port: 8080 +# - path: /django-rq +# pathType: "Prefix" +# service: +# name: backend-service +# port: 8080 +# - path: /git +# pathType: "Prefix" +# service: +# name: backend-service +# port: 8080 +# - path: /opencv +# pathType: "Prefix" +# service: +# name: backend-service +# port: 8080 +# - path: /profiler +# pathType: "Prefix" +# service: +# name: backend-service +# port: 8080 +# - path : / +# pathType: "Prefix" +# service: +# name: frontend-service +# port: 80 +# tls: +# - hosts: +# - +# secretName: ingress-tls-cvat traefik: + enabled: false service: externalIPs: # - "192.168.49.2" diff --git a/package.json b/package.json index 14efb012c4ae..5bcd24f5da12 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "babel-register": "^6.26.0", "bundle-declarations-webpack-plugin": "^3.1.0", "copy-webpack-plugin": "^11.0.0", - "css-loader": "^3.4.2", + "css-loader": "^6.8.1", "eslint": "^7.11.0", "eslint-config-airbnb": "^18.0.1", "eslint-config-airbnb-base": "14.2.1", @@ -54,8 +54,9 @@ "micromatch": "^4.0.2", "nodemon": "^2.0.7", "nyc": "^15.1.0", - "postcss-loader": "^3.0.0", - "postcss-preset-env": "^6.7.0", + "postcss": "8", + "postcss-loader": "^7.3.3", + "postcss-preset-env": "^9.0.0", "react-svg-loader": "^3.0.3", "remark-cli": "^9.0.0", "remark-frontmatter": "^3.0.0", @@ -76,8 +77,8 @@ "sass-loader": "^10.0.0", "source-map-support": "^0.5.19", "style-loader": "^1.0.0", - "stylelint": "^13.6.1", - "stylelint-config-standard": "^20.0.0", + "stylelint": "^15.10.2", + "stylelint-config-standard-scss": "^10.0.0", "typescript": "^5.0.2", "vfile-reporter-json": "^2.0.2", "webpack": "^5.76.0", diff --git a/site/content/en/docs/manual/advanced/annotation-quality.md b/site/content/en/docs/manual/advanced/annotation-quality.md new file mode 100644 index 000000000000..dd8cee145491 --- /dev/null +++ b/site/content/en/docs/manual/advanced/annotation-quality.md @@ -0,0 +1,232 @@ +--- +title: 'Annotation quality & Honeypot' +linkTitle: 'Annotation quality' +weight: 14 +description: 'How to check the quality of annotation in CVAT' +--- + +In CVAT, it's possible to evaluate the quality of annotation through +the creation of a **Ground truth** job, referred to as a Honeypot. +To estimate the task quality, CVAT compares all other jobs in the task against the +established **Ground truth** job, and calculates annotation quality +based on this comparison. + +> **Note** that quality estimation only supports +> 2d tasks. It supports all the annotation types except 2d cuboids. + +> **Note** that tracks are considered separate shapes +> and compared on a per-frame basis with other tracks and shapes. + +See: + +- [Ground truth job](#ground-truth-job) +- [Managing Ground Truth jobs: Import, Export, and Deletion](#managing-ground-truth-jobs-import-export-and-deletion) + - [Import](#import) + - [Export](#export) + - [Delete](#delete) +- [Assessing data quality with Ground truth jobs](#assessing-data-quality-with-ground-truth-jobs) + - [Quality data](#quality-data) + - [Annotation quality settings](#annotation-quality-settings) + - [GT conflicts in the CVAT interface](#gt-conflicts-in-the-cvat-interface) +- [Annotation quality \& Honeypot video tutorial](#annotation-quality--honeypot-video-tutorial) + +## Ground truth job + +A **Ground truth** job is a way to tell CVAT where to store +and get the "correct" annotations for task quality estimation. + +To estimate task quality, you need to +create a **Ground truth** job in the task, +and annotate it. You don’t need to +annotate the whole dataset twice, +the annotation quality of a small part of +the data shows the quality of annotation for +the whole dataset. + +For the quality assurance to function correctly, the **Ground truth** job must +have a small portion of the task frames and the frames must be chosen randomly. +Depending on the dataset size and task complexity, +**5-15% of the data is typically good enough** for quality estimation, +while keeping extra annotation overhead acceptable. + +For example, in a typical **task with 2000 frames**, selecting **just 5%**, +which is 100 extra frames to annotate, **is enough** to estimate the +annotation quality. If the task contains **only 30 frames**, it's advisable to +select **8-10 frames**, which is **about 30%**. + +It is more than 15% but in the case of smaller datasets, +we need more samples to estimate quality reliably. + +To create a **Ground truth** job, do the following: + +1. Create a [task](/docs/manual/basics/create_an_annotation_task/), and open the task page. +2. Click **+**. + + ![Create job](/images/honeypot01.jpg) + +3. In the **Add new job** window, fill in the following fields: + + ![Add new job](/images/honeypot02.jpg) + + - **Job type**: Use the default parameter **Ground truth**. + - **Frame selection method**: Use the default parameter **Random**. + - **Quantity %**: Set the desired percentage of frames for the **Ground truth** job. +
**Note** that when you use **Quantity %**, the **Frames** field will be autofilled. + - **Frame count**: Set the desired number of frames for the "ground truth" job. +
**Note** that when you use **Frames**, the **Quantity %** field will be will be autofilled. + - **Seed**: (Optional) If you need to make the random selection reproducible, specify this number. + It can be any integer number, the same value will yield the same random selection (given that the + frame number is unchanged).
**Note** that if you want to use a + custom frame sequence, you can do this using the server API instead, + see [Jobs API #create](https://opencv.github.io/cvat/docs/api_sdk/sdk/reference/apis/jobs-api/#create). + +4. Click **Submit**. +5. Annotate frames, save your work. +6. Change the status of the job to **Completed**. +7. Change **Stage** to **Accepted**. + +The **Ground truth** job will appear in the jobs list. + +![Add new job](/images/honeypot03.jpg) + +## Managing Ground Truth jobs: Import, Export, and Deletion + +Annotations from **Ground truth** jobs are not included in the dataset export, +they also cannot be imported during task annotations import +or with automatic annotation for the task. + +Import, export, and delete options are available from the +job's menu. + +![Add new job](/images/honeypot04.jpg) + +### Import + +If you want to import annotations into the **Ground truth** job, do the following. + +1. Open the task, and find the **Ground truth** job in the jobs list. +2. Click on three dots to open the menu. +3. From the menu, select **Import annotations**. +4. Select import format, and select file. +5. Click **OK**. + +> **Note** that if there are imported annotations for the frames that exist in the task, +> but are not included in the **Ground truth** job, they will be ignored. +> This way, you don't need to worry about "cleaning up" your **Ground truth** +> annotations for the whole dataset before importing them. +> Importing annotations for the frames that are not known in the task still raises errors. + +### Export + +To export annotations from the **Ground truth** job, do the following. + +1. Open the task, and find a job in the jobs list. +2. Click on three dots to open the menu. +3. From the menu, select **Export annotations**. + +### Delete + +To delete the **Ground truth** job, do the following. + +1. Open the task, and find the **Ground truth** job in the jobs list. +2. Click on three dots to open the menu. +3. From the menu, select **Delete**. + +## Assessing data quality with Ground truth jobs + +Once you've established the **Ground truth** job, proceed to annotate the dataset. + +CVAT will begin the quality comparison between the annotated task and the +**Ground truth** job in this task once it is finished (on the `acceptance` stage and in the `completed` state). + +> **Note** that the process of quality calculation may take up to several hours, depending on +> the amount of data and labeled objects, and is **not updated immediately** after task updates. + +To view results go to the **Task** > **Actions** > **View analytics**> **Performance** tab. + +![Add new job](/images/honeypot05.jpg) + +### Quality data + +The Analytics page has the following fields: + + + +| Field | Description | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Mean annotation quality | Displays the average quality of annotations, which includes: the count of accurate annotations, total task annotations, ground truth annotations, accuracy rate, precision rate, and recall rate. | +| GT Conflicts | Conflicts identified during quality assessment, including extra or missing annotations. Mouse over the **?** icon for a detailed conflict report on your dataset. | +| Issues | Number of [opened issues](/docs/manual/advanced/review/). If no issues were reported, will show 0. | +| Quality report | Quality report in JSON format. | +| Ground truth job data | "Information about ground truth job, including date, time, and number of issues. | +| List of jobs | List of all the jobs in the task | + + + +### Annotation quality settings + +If you need to tweak some aspects of comparisons, you can do this from +the **Annotation Quality Settings** menu. + +You can configure what overlap +should be considered low or how annotations must be compared. + +The updated settings will take effect +on the next quality update. + +To open **Annotation Quality Settings**, find +**Quality report** and on the right side of it, click on +three dots. + +The following window will open. +Hover over the **?** marks to understand what each field represents. + +![Add new job](/images/honeypot08.jpg) + +Annotation quality settings have the following parameters: + + + +| Field | Description | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Min overlap threshold | Min overlap threshold(IoU) is used for the distinction between matched / unmatched shapes. | +| Low overlap threshold | Low overlap threshold is used for the distinction between strong/weak (low overlap) matches. | +| OKS Sigma | IoU threshold for points. The percent of the box area, used as the radius of the circle around the GT point, where the checked point is expected to be. | +| Relative thickness (frame side %) | Thickness of polylines, relative to the (image area) ^ 0.5. The distance to the boundary around the GT line inside of which the checked line points should be. | +| Check orientation | Indicates that polylines have direction. | +| Min similarity gain (%) | The minimal gain in the GT IoU between the given and reversed line directions to consider the line inverted. Only useful with the Check orientation parameter. | +| Compare groups | Enables or disables annotation group checks. | +| Min group match threshold | Minimal IoU for groups to be considered matching, used when the Compare groups are enabled. | +| Check object visibility | Check for partially-covered annotations. Masks and polygons will be compared to each other. | +| Min visibility threshold | Minimal visible area percent of the spatial annotations (polygons, masks) | +| For reporting covered annotations, useful with the Check object visibility option. | +| Match only visible parts | Use only the visible part of the masks and polygons in comparisons. | + + + +### GT conflicts in the CVAT interface + +To see GT Conflicts in the CVAT interface, go to **Review** > +**Issues** > **Show ground truth annotations and conflicts**. + +![GT conflict](/images/honeypot06.gif) + +The ground truth (GT) annotation is depicted as +a dotted-line box with an associated label. + +Upon hovering over an issue on the right-side panel with your mouse, +the corresponding GT Annotation gets highlighted. + +Use arrows in the Issue toolbar to move between GT conflicts. + +To create an issue related to the conflict, +right-click on the bounding box and from the +menu select the type of issue you want to create. + +![GT conflict](/images/honeypot07.jpg) + +## Annotation quality & Honeypot video tutorial + +This video demonstrates the process: + + diff --git a/site/content/en/docs/manual/advanced/automatic-annotation.md b/site/content/en/docs/manual/advanced/automatic-annotation.md index 543e85edf1fb..3805509d7ad5 100644 --- a/site/content/en/docs/manual/advanced/automatic-annotation.md +++ b/site/content/en/docs/manual/advanced/automatic-annotation.md @@ -2,54 +2,117 @@ title: 'Automatic annotation' linkTitle: 'Automatic annotation' weight: 16 -description: 'Guide to using the automatic annotation of tasks.' +description: 'Automatic annotation of tasks' --- -Automatic Annotation is used for creating preliminary annotations. -To use Automatic Annotation you need a DL model that can be deployed by a CVAT administrator. -You can find the list of available models in the `Models` section. +Automatic annotation in CVAT is a tool that you can use +to automatically pre-annotate your data with pre-trained models. -1. To launch automatic annotation, you should open the dashboard and find a task which you want to annotate. - Then click the `Actions` button and choose option `Automatic Annotation` from the dropdown menu. +CVAT can use models from the following sources: + +- [Pre-installed models](#models). +- Models integrated from [Hugging Face and Roboflow](#adding-models-from-hugging-face-and-roboflow). +- [Self-hosted models deployed with Nuclio](/docs/manual/advanced/serverless-tutorial/). + +The following table describes the available options: + +| | Self-hosted | Cloud | +| ------------------------------------------- | ---------------------- | ------------------------------------------------ | +| **Price** | Free | See [Pricing](https://www.cvat.ai/pricing/cloud) | +| **Models** | You have to add models | You can use pre-installed models | +| **Hugging Face & Roboflow
integration** | Not supported | Supported | + +See: + +- [Running Automatic annotation](#running-automatic-annotation) +- [Labels matching](#labels-matching) +- [Models](#models) +- [Adding models from Hugging Face and Roboflow](#adding-models-from-hugging-face-and-roboflow) + +## Running Automatic annotation + +To start automatic annotation, do the following: + +1. On the top menu, click **Tasks**. +1. Find the task you want to annotate and click **Action** > **Automatic annotation**. ![](/images/image119_detrac.jpg) -1. In the dialog window select a model you need. DL models are created for specific labels, e.g. - the Crossroad model was taught using footage from cameras located above the highway and it is best to - use this model for the tasks with similar camera angles. - If it's necessary select the `Clean old annotations` checkbox. - Adjust the labels so that the task labels will correspond to the labels of the DL model. - For example, let’s consider a task where you have to annotate labels “car” and “person”. - You should connect the “person” label from the model to the “person” label in the task. - As for the “car” label, you should choose the most fitting label available in the model - the “vehicle” label. - If the chosen model supports automatic attributes detection - (like facial expressions, for example: ``serverless/openvino/omz/intel/face-detection-0205``), - you can also map attributes between the DL model and your task. - The task requires to annotate cars only and choosing the “vehicle” label implies annotation of all vehicles, - in this case using auto annotation will help you complete the task faster. - Click `Submit` to begin the automatic annotation process. +1. In the Automatic annotation dialog, from the drop-down list, select a [model](#models). +1. [Match the labels](#labels-matching) of the model and the task. +1. (Optional) In case you need the model to return masks as polygons, switch toggle **Return masks as polygons**. +1. (Optional) In case you need to remove all previous annotations, switch toggle **Clean old annotations**. ![](/images/image120.jpg) -1. At runtime - you can see the percentage of completion. - You can cancel the automatic annotation by clicking on the `Cancel`button. +1. Click **Annotate**. + +CVAT will show the progress of annotation on the progress bar. + +![Progress bar](/images/image121_detrac.jpg) + +You can stop the automatic annotation at any moment by clicking cancel. + +## Labels matching + +Each model is trained on a dataset and supports only the dataset's labels. + +For example: + +- DL model has the label `car`. +- Your task (or project) has the label `vehicle`. + +To annotate, you need to match these two labels to give +CVAT a hint that, in this case, `car` = `vehicle`. + +If you have a label that is not on the list +of DL labels, you will not be able to +match them. + +For this reason, supported DL models are suitable only +for certain labels. + +To check the list of labels for each model, see [Models](#models) +papers and official documentation. + +## Models + +Automatic annotation uses pre-installed and added models. + +> For self-hosted solutions, +> you need to [install Automatic Annotation first](/docs/administration/advanced/installation_automatic_annotation/) +> and [add models](/docs/manual/advanced/models/). + +List of pre-installed models: + + + +| Model | Description | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Attributed face detection | Three OpenVINO models work together:

  • [Face Detection 0205](https://docs.openvino.ai/2022.3/omz_models_model_face_detection_0205.html): face detector based on MobileNetV2 as a backbone with a FCOS head for indoor and outdoor scenes shot by a front-facing camera.
  • [Emotions recognition retail 0003](https://docs.openvino.ai/2022.3/omz_models_model_emotions_recognition_retail_0003.html#emotions-recognition-retail-0003): fully convolutional network for recognition of five emotions (‘neutral’, ‘happy’, ‘sad’, ‘surprise’, ‘anger’).
  • [Age gender recognition retail 0013](https://docs.openvino.ai/2022.3/omz_models_model_age_gender_recognition_retail_0013.html): fully convolutional network for simultaneous Age/Gender recognition. The network can recognize the age of people in the [18 - 75] years old range; it is not applicable for children since their faces were not in the training set. | +| RetinaNet R101 | RetinaNet is a one-stage object detection model that utilizes a focal loss function to address class imbalance during training. Focal loss applies a modulating term to the cross entropy loss to focus learning on hard negative examples. RetinaNet is a single, unified network composed of a backbone network and two task-specific subnetworks.

    For more information, see:
  • [Site: RetinaNET](https://paperswithcode.com/lib/detectron2/retinanet) | +| Text detection | Text detector based on PixelLink architecture with MobileNetV2, depth_multiplier=1.4 as a backbone for indoor/outdoor scenes.

    For more information, see:
  • [Site: OpenVINO Text detection 004](https://docs.openvino.ai/2022.3/omz_models_model_text_detection_0004.html) | +| YOLO v3 | YOLO v3 is a family of object detection architectures and models pre-trained on the COCO dataset.

    For more information, see:
  • [Site: YOLO v3](https://docs.openvino.ai/2022.3/omz_models_model_yolo_v3_tf.html) | +| YOLO v5 | YOLO v5 is a family of object detection architectures and models based on the Pytorch framework.

    For more information, see:
  • [GitHub: YOLO v5](https://github.com/ultralytics/yolov5)
  • [Site: YOLO v5](https://docs.ultralytics.com/#yolov5) | +| YOLO v7 | YOLOv7 is an advanced object detection model that outperforms other detectors in terms of both speed and accuracy. It can process frames at a rate ranging from 5 to 160 frames per second (FPS) and achieves the highest accuracy with 56.8% average precision (AP) among real-time object detectors running at 30 FPS or higher on the V100 graphics processing unit (GPU).

    For more information, see:
  • [GitHub: YOLO v7](https://github.com/WongKinYiu/yolov7)
  • [Paper: YOLO v7](https://arxiv.org/pdf/2207.02696.pdf) | + + - ![](/images/image121_detrac.jpg) +## Adding models from Hugging Face and Roboflow -1. The end result of an automatic annotation is an annotation with separate rectangles (or other shapes) +In case you did not find the model you need, you can add a model +of your choice from [Hugging Face](https://huggingface.co/) +or [Roboflow](https://roboflow.com/). - ![](/images/gif014_detrac.gif) +> **Note**, that you cannot add models from Hugging Face and Roboflow to self-hosted CVAT. -1. You can combine separate bounding boxes into tracks using the `Person reidentification ` model. - To do this, click on the automatic annotation item in the action menu again and select the model - of the `ReID` type (in this case the `Person reidentification` model). - You can set the following parameters: + - - Model `Threshold` is a maximum cosine distance between objects’ embeddings. - - `Maximum distance` defines a maximum radius that an object can diverge between adjacent frames. +For more information, +see [Streamline annotation by integrating Hugging Face and Roboflow models](https://www.cvat.ai/post/integrating-hugging-face-and-roboflow-models). - ![](/images/image133.jpg) +This video demonstrates the process: -1. You can remove false positives and edit tracks using `Split` and `Merge` functions. + - ![](/images/gif015_detrac.gif) + diff --git a/site/content/en/images/gif014_detrac.gif b/site/content/en/images/gif014_detrac.gif deleted file mode 100644 index a80bcc54c762..000000000000 Binary files a/site/content/en/images/gif014_detrac.gif and /dev/null differ diff --git a/site/content/en/images/gif015_detrac.gif b/site/content/en/images/gif015_detrac.gif deleted file mode 100644 index f711ce5aaf99..000000000000 Binary files a/site/content/en/images/gif015_detrac.gif and /dev/null differ diff --git a/site/content/en/images/honeypot01.jpg b/site/content/en/images/honeypot01.jpg new file mode 100644 index 000000000000..ee147c3359c6 Binary files /dev/null and b/site/content/en/images/honeypot01.jpg differ diff --git a/site/content/en/images/honeypot02.jpg b/site/content/en/images/honeypot02.jpg new file mode 100644 index 000000000000..ea08371dafe0 Binary files /dev/null and b/site/content/en/images/honeypot02.jpg differ diff --git a/site/content/en/images/honeypot03.jpg b/site/content/en/images/honeypot03.jpg new file mode 100644 index 000000000000..b536fdd448cc Binary files /dev/null and b/site/content/en/images/honeypot03.jpg differ diff --git a/site/content/en/images/honeypot04.jpg b/site/content/en/images/honeypot04.jpg new file mode 100644 index 000000000000..8e21d2640490 Binary files /dev/null and b/site/content/en/images/honeypot04.jpg differ diff --git a/site/content/en/images/honeypot05.jpg b/site/content/en/images/honeypot05.jpg new file mode 100644 index 000000000000..69fdc0d4bf7c Binary files /dev/null and b/site/content/en/images/honeypot05.jpg differ diff --git a/site/content/en/images/honeypot06.gif b/site/content/en/images/honeypot06.gif new file mode 100644 index 000000000000..9af0b4b8a7bb Binary files /dev/null and b/site/content/en/images/honeypot06.gif differ diff --git a/site/content/en/images/honeypot07.jpg b/site/content/en/images/honeypot07.jpg new file mode 100644 index 000000000000..4fa264465a93 Binary files /dev/null and b/site/content/en/images/honeypot07.jpg differ diff --git a/site/content/en/images/honeypot08.jpg b/site/content/en/images/honeypot08.jpg new file mode 100644 index 000000000000..fba030b1ff58 Binary files /dev/null and b/site/content/en/images/honeypot08.jpg differ diff --git a/site/content/en/images/image120.jpg b/site/content/en/images/image120.jpg index c0cca3fa1456..3eb964fe2f91 100644 Binary files a/site/content/en/images/image120.jpg and b/site/content/en/images/image120.jpg differ diff --git a/site/content/en/images/image133.jpg b/site/content/en/images/image133.jpg deleted file mode 100644 index f869057fa1d5..000000000000 Binary files a/site/content/en/images/image133.jpg and /dev/null differ diff --git a/supervisord/worker.analytics_reports.conf b/supervisord/worker.analytics_reports.conf new file mode 100644 index 000000000000..7718a9202db5 --- /dev/null +++ b/supervisord/worker.analytics_reports.conf @@ -0,0 +1,27 @@ +[unix_http_server] +file = /tmp/supervisord/supervisor.sock + +[supervisorctl] +serverurl = unix:///tmp/supervisord/supervisor.sock + + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisord] +nodaemon=true +logfile=%(ENV_HOME)s/logs/supervisord.log ; supervisord log file +logfile_maxbytes=50MB ; maximum size of logfile before rotation +logfile_backups=10 ; number of backed up logfiles +loglevel=debug ; info, debug, warn, trace +pidfile=/tmp/supervisord/supervisord.pid ; pidfile location +childlogdir=%(ENV_HOME)s/logs/ ; where child log files will live + +[program:rqworker_analytics_reports] +command=%(ENV_HOME)s/wait-for-it.sh %(ENV_CVAT_REDIS_HOST)s:6379 -t 0 -- bash -ic " \ + exec python3 %(ENV_HOME)s/manage.py rqworker -v 3 analytics_reports \ + --worker-class cvat.rqworker.DefaultWorker \ + " +environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler" +numprocs=%(ENV_NUMPROCS)s +process_name=%(program_name)s-%(process_num)s diff --git a/tests/cypress/e2e/actions_objects/case_37_object_make_copy.js b/tests/cypress/e2e/actions_objects/case_37_object_make_copy.js index b045bce1e9fc..70d90dc275a2 100644 --- a/tests/cypress/e2e/actions_objects/case_37_object_make_copy.js +++ b/tests/cypress/e2e/actions_objects/case_37_object_make_copy.js @@ -59,10 +59,10 @@ context('Object make a copy.', () => { const createEllipseShape = { type: 'Shape', labelName, - cx: 550, - cy: 100, - rightX: 600, - topY: 150, + firstX: 550, + firstY: 100, + secondX: 600, + secondY: 150, }; const countObject = 6; diff --git a/tests/cypress/e2e/actions_objects/case_99_save_filtered_object_in_AAM.js b/tests/cypress/e2e/actions_objects/case_99_save_filtered_object_in_AAM.js index 9f1c613bc772..6f72aaf6219f 100644 --- a/tests/cypress/e2e/actions_objects/case_99_save_filtered_object_in_AAM.js +++ b/tests/cypress/e2e/actions_objects/case_99_save_filtered_object_in_AAM.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -26,7 +27,7 @@ context('Save filtered object in AAM.', () => { // Getting list of labels and create a label if neccessary const labelsList = Array.from(doc.querySelectorAll('.cvat-constructor-viewer-item')); if (labelsList.length < 2) { - cy.addNewLabel(newLabelName); + cy.addNewLabel({ name: newLabelName }); } }); cy.document().then((doc) => { diff --git a/tests/cypress/e2e/actions_objects2/case_10_polygon_shape_track_label_points.js b/tests/cypress/e2e/actions_objects2/case_10_polygon_shape_track_label_points.js index b9f781b83841..e081e9751868 100644 --- a/tests/cypress/e2e/actions_objects2/case_10_polygon_shape_track_label_points.js +++ b/tests/cypress/e2e/actions_objects2/case_10_polygon_shape_track_label_points.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -80,7 +81,7 @@ context('Actions on polygon.', () => { before(() => { cy.openTask(taskName); - cy.addNewLabel(newLabelName); + cy.addNewLabel({ name: newLabelName }); cy.openJob(); }); diff --git a/tests/cypress/e2e/actions_objects2/case_115_ellipse_shape_track_label.js b/tests/cypress/e2e/actions_objects2/case_115_ellipse_shape_track_label.js index 942a43bf83d3..251f8265e504 100644 --- a/tests/cypress/e2e/actions_objects2/case_115_ellipse_shape_track_label.js +++ b/tests/cypress/e2e/actions_objects2/case_115_ellipse_shape_track_label.js @@ -1,4 +1,5 @@ // Copyright (C) 2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -13,34 +14,34 @@ context('Actions on ellipse.', () => { const createEllipseShape = { type: 'Shape', labelName, - cx: 250, - cy: 350, - rightX: 450, - topY: 280, + firstX: 250, + firstY: 350, + secondX: 450, + secondY: 280, }; const createEllipseTrack = { type: 'Track', labelName, - cx: createEllipseShape.cx, - cy: createEllipseShape.cy - 150, - rightX: createEllipseShape.rightX, - topY: createEllipseShape.topY - 150, + firstX: createEllipseShape.firstX, + firstY: createEllipseShape.firstY - 150, + secondX: createEllipseShape.secondX, + secondY: createEllipseShape.secondY - 150, }; const createEllipseShapeSwitchLabel = { type: 'Shape', labelName: newLabelName, - cx: createEllipseShape.cx + 250, - cy: createEllipseShape.cy, - rightX: createEllipseShape.rightX + 250, - topY: createEllipseShape.topY, + firstX: createEllipseShape.firstX + 250, + firstY: createEllipseShape.firstY, + secondX: createEllipseShape.secondX + 250, + secondY: createEllipseShape.secondY, }; const createEllipseTrackSwitchLabel = { type: 'Track', labelName: newLabelName, - cx: createEllipseShape.cx + 250, - cy: createEllipseShape.cy - 150, - rightX: createEllipseShape.rightX + 250, - topY: createEllipseShape.topY - 150, + firstX: createEllipseShape.firstX + 250, + firstY: createEllipseShape.firstY - 150, + secondX: createEllipseShape.secondX + 250, + secondY: createEllipseShape.secondY - 150, }; function testCompareRotate(shape, toFrame) { @@ -58,7 +59,7 @@ context('Actions on ellipse.', () => { before(() => { cy.openTask(taskName); - cy.addNewLabel(newLabelName); + cy.addNewLabel({ name: newLabelName }); cy.openJob(); }); diff --git a/tests/cypress/e2e/actions_objects2/case_11_polylines_shape_track_label_points.js b/tests/cypress/e2e/actions_objects2/case_11_polylines_shape_track_label_points.js index 6bbb5327f379..ea0fd8906346 100644 --- a/tests/cypress/e2e/actions_objects2/case_11_polylines_shape_track_label_points.js +++ b/tests/cypress/e2e/actions_objects2/case_11_polylines_shape_track_label_points.js @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// +// Copyright (C) 2023 CVAT.ai Corporation // SPDX-License-Identifier: MIT /// @@ -74,7 +74,7 @@ context('Actions on polylines.', () => { before(() => { cy.openTask(taskName); - cy.addNewLabel(newLabelName); + cy.addNewLabel({ name: newLabelName }); cy.openJob(); }); diff --git a/tests/cypress/e2e/actions_objects2/case_12_points_shape_track_label.js b/tests/cypress/e2e/actions_objects2/case_12_points_shape_track_label.js index b5e2845205e2..462d80467657 100644 --- a/tests/cypress/e2e/actions_objects2/case_12_points_shape_track_label.js +++ b/tests/cypress/e2e/actions_objects2/case_12_points_shape_track_label.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -74,7 +75,7 @@ context('Actions on points.', () => { before(() => { cy.openTask(taskName); - cy.addNewLabel(newLabelName); + cy.addNewLabel({ name: newLabelName }); cy.openJob(); }); diff --git a/tests/cypress/e2e/actions_objects2/case_17_lock_hide_features.js b/tests/cypress/e2e/actions_objects2/case_17_lock_hide_features.js index 2eccb8941fe4..a1cde9a3048b 100644 --- a/tests/cypress/e2e/actions_objects2/case_17_lock_hide_features.js +++ b/tests/cypress/e2e/actions_objects2/case_17_lock_hide_features.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -95,10 +96,9 @@ context('Lock/hide features.', () => { before(() => { cy.openTask(taskName); - cy.addNewLabel(newLabelName1); - cy.addNewLabel(newLabelName2); - cy.addNewLabel(newLabelName3); - cy.addNewLabel(newLabelName4); + [newLabelName1, newLabelName2, newLabelName3, newLabelName4].forEach((name) => { + cy.addNewLabel({ name }); + }); cy.openJob(); }); diff --git a/tests/cypress/e2e/actions_objects2/case_8_rectangle_shape_track_label.js b/tests/cypress/e2e/actions_objects2/case_8_rectangle_shape_track_label.js index abd9bf5f9c87..8f866fe9f80b 100644 --- a/tests/cypress/e2e/actions_objects2/case_8_rectangle_shape_track_label.js +++ b/tests/cypress/e2e/actions_objects2/case_8_rectangle_shape_track_label.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -78,7 +79,7 @@ context('Actions on rectangle', () => { before(() => { cy.openTask(taskName); - cy.addNewLabel(newLabelName); + cy.addNewLabel({ name: newLabelName }); cy.openJob(); }); diff --git a/tests/cypress/e2e/actions_objects2/case_9_cuboid_shape_track_label.js b/tests/cypress/e2e/actions_objects2/case_9_cuboid_shape_track_label.js index 6f542a16bd3a..5d96b059acf6 100644 --- a/tests/cypress/e2e/actions_objects2/case_9_cuboid_shape_track_label.js +++ b/tests/cypress/e2e/actions_objects2/case_9_cuboid_shape_track_label.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -78,7 +79,7 @@ context('Actions on Cuboid', () => { before(() => { cy.openTask(taskName); - cy.addNewLabel(newLabelName); + cy.addNewLabel({ name: newLabelName }); cy.openJob(); }); diff --git a/tests/cypress/e2e/actions_projects_models/registration_involved/base_actions_project_task_user.js b/tests/cypress/e2e/actions_projects_models/registration_involved/base_actions_project_task_user.js index 7bf7bf4c37ae..970215305c22 100644 --- a/tests/cypress/e2e/actions_projects_models/registration_involved/base_actions_project_task_user.js +++ b/tests/cypress/e2e/actions_projects_models/registration_involved/base_actions_project_task_user.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -65,10 +66,9 @@ context('Base actions on the project', () => { describe('Testing "Base actions on the project"', () => { it('Add some labels to project.', () => { - cy.addNewLabel(newLabelName1); - cy.addNewLabel(newLabelName2); - cy.addNewLabel(newLabelName3); - cy.addNewLabel(newLabelName4); + [newLabelName1, newLabelName2, newLabelName3, newLabelName4].forEach((name) => { + cy.addNewLabel({ name }); + }); }); it('Create a first task for the project. Project field is completed with proper project name and labels editor is not accessible.', () => { cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount); diff --git a/tests/cypress/e2e/actions_tasks/case_61_create_task_set_issue_tracker.js b/tests/cypress/e2e/actions_tasks/case_61_create_task_set_issue_tracker.js index eb8332b4fa8c..80bdf56f6faa 100644 --- a/tests/cypress/e2e/actions_tasks/case_61_create_task_set_issue_tracker.js +++ b/tests/cypress/e2e/actions_tasks/case_61_create_task_set_issue_tracker.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -39,7 +40,7 @@ context('Create a task with set an issue tracker.', () => { describe(`Testing "${labelName}"`, () => { it('Creating a task with incorrect issue tracker URL. The error notification is shown.', () => { cy.get('[id="name"]').type(taskName); - cy.addNewLabel(labelName); + cy.addNewLabel({ name: labelName }); cy.get('input[type="file"]').attachFile(archiveName, { subjectType: 'drag-n-drop' }); cy.contains('Advanced configuration').click(); cy.get('#bugTracker').type(incorrectBugTrackerUrl); diff --git a/tests/cypress/e2e/actions_tasks/case_66_rename_label_raw_editor.js b/tests/cypress/e2e/actions_tasks/case_66_rename_label_raw_editor.js index 7c714159dd34..f41752579aea 100644 --- a/tests/cypress/e2e/actions_tasks/case_66_rename_label_raw_editor.js +++ b/tests/cypress/e2e/actions_tasks/case_66_rename_label_raw_editor.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -43,7 +44,7 @@ context('Rename a label via raw editor.', () => { cy.createZipArchive(directoryToArchive, archivePath); cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, archiveName); cy.openTask(taskName); - cy.addNewLabel(labelName); + cy.addNewLabel({ name: labelName }); }); after(() => { diff --git a/tests/cypress/e2e/actions_tasks/case_72_hotkeys_change_labels.js b/tests/cypress/e2e/actions_tasks/case_72_hotkeys_change_labels.js index cdabfcab3e67..d0b286e322d7 100644 --- a/tests/cypress/e2e/actions_tasks/case_72_hotkeys_change_labels.js +++ b/tests/cypress/e2e/actions_tasks/case_72_hotkeys_change_labels.js @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -24,9 +24,9 @@ context('Hotkeys to change labels feature.', () => { const directoryToArchive = imagesFolder; const secondLabel = `Case ${caseId} second`; const additionalAttrsSecondLabel = [{ - additionalAttrName: attrName, - additionalValue: '0;3;1', - typeAttribute: 'Number', + name: attrName, + values: '0;3;1', + type: 'Number', mutable: false, }]; let firstLabelCurrentVal = ''; @@ -54,7 +54,7 @@ context('Hotkeys to change labels feature.', () => { cy.createZipArchive(directoryToArchive, archivePath); cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, archiveName); cy.openTask(taskName); - cy.addNewLabel(secondLabel, additionalAttrsSecondLabel); + cy.addNewLabel({ name: secondLabel }, additionalAttrsSecondLabel); cy.openJob(); }); diff --git a/tests/cypress/e2e/actions_tasks/mutable_attributes.js b/tests/cypress/e2e/actions_tasks/mutable_attributes.js index 27b40aa92b7d..7f6d17d5854d 100644 --- a/tests/cypress/e2e/actions_tasks/mutable_attributes.js +++ b/tests/cypress/e2e/actions_tasks/mutable_attributes.js @@ -11,7 +11,7 @@ context('Mutable attribute.', () => { const labelName = 'car'; const additionalAttrsLabelShape = [ { - additionalAttrName: 'tree', additionalValue: 'birch tree', typeAttribute: 'Text', mutable: true, + name: 'tree', values: 'birch tree', type: 'Text', mutable: true, }, ]; @@ -31,7 +31,7 @@ context('Mutable attribute.', () => { function testChangingAttributeValue(expectedValue, value) { cy.get('.cvat-player-next-button').click(); cy.get('.attribute-annotation-sidebar-attr-elem-wrapper') - .find('[type="text"]') + .find('textarea') .should('have.value', expectedValue) .clear() .type(value); @@ -45,7 +45,7 @@ context('Mutable attribute.', () => { before(() => { cy.openTask(taskName); - cy.addNewLabel(labelName, additionalAttrsLabelShape); + cy.addNewLabel({ name: labelName }, additionalAttrsLabelShape); cy.openJob(); cy.createRectangle(createRectangleTrack2Points); }); @@ -54,7 +54,7 @@ context('Mutable attribute.', () => { it('Go to AAM. For the 2nd and 3rd frames, change the attribute value.', () => { cy.changeWorkspace('Attribute annotation'); cy.changeLabelAAM(labelName); - testChangingAttributeValue(additionalAttrsLabelShape[0].additionalValue, attrValueSecondFrame); + testChangingAttributeValue(additionalAttrsLabelShape[0].values, attrValueSecondFrame); testChangingAttributeValue(attrValueSecondFrame, attrValueThirdFrame); }); @@ -66,12 +66,12 @@ context('Mutable attribute.', () => { [ [ 0, - `${additionalAttrsLabelShape[0].additionalAttrName}: ${ - additionalAttrsLabelShape[0].additionalValue}`, + `${additionalAttrsLabelShape[0].name}: ${ + additionalAttrsLabelShape[0].values}`, ], - [1, `${additionalAttrsLabelShape[0].additionalAttrName}: ${attrValueSecondFrame}`], - [2, `${additionalAttrsLabelShape[0].additionalAttrName}: ${attrValueThirdFrame}`], - [3, `${additionalAttrsLabelShape[0].additionalAttrName}: ${attrValueThirdFrame}`], + [1, `${additionalAttrsLabelShape[0].name}: ${attrValueSecondFrame}`], + [2, `${additionalAttrsLabelShape[0].name}: ${attrValueThirdFrame}`], + [3, `${additionalAttrsLabelShape[0].name}: ${attrValueThirdFrame}`], ].forEach(([num, val]) => { checkObjectDetailValue(num, val); }); diff --git a/tests/cypress/e2e/actions_tasks/continue_frame_n.js b/tests/cypress/e2e/actions_tasks/navigate_specific_frame.js similarity index 80% rename from tests/cypress/e2e/actions_tasks/continue_frame_n.js rename to tests/cypress/e2e/actions_tasks/navigate_specific_frame.js index a369ad969ef0..ea34133eebd4 100644 --- a/tests/cypress/e2e/actions_tasks/continue_frame_n.js +++ b/tests/cypress/e2e/actions_tasks/navigate_specific_frame.js @@ -6,7 +6,7 @@ context('Paste labels from one task to another.', { browser: '!firefox' }, () => { const task = { - name: 'Test "Continue frame N"', + name: 'Test "Continue/open frame N"', label: 'Test label', attrName: 'Test attribute', attrValue: 'Test attribute value', @@ -54,5 +54,15 @@ context('Paste labels from one task to another.', { browser: '!firefox' }, () => cy.get('.cvat-notification-continue-job-button').click(); cy.checkFrameNum(2); }); + + it('Trying to open a frame using query parameter', () => { + cy.url().then(($url) => { + cy.visit('/projects'); + cy.get('.cvat-projects-page').should('exist'); + cy.visit($url, { qs: { frame: 2 } }); + cy.get('.cvat-canvas-container').should('exist').and('be.visible'); + cy.checkFrameNum(2); + }); + }); }); }); diff --git a/tests/cypress/e2e/actions_tasks2/case_31_label_constructor_color_name_label.js b/tests/cypress/e2e/actions_tasks2/case_31_label_constructor_color_name_label.js index 3547f9367672..46c9c6331044 100644 --- a/tests/cypress/e2e/actions_tasks2/case_31_label_constructor_color_name_label.js +++ b/tests/cypress/e2e/actions_tasks2/case_31_label_constructor_color_name_label.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -59,9 +60,9 @@ context('Label constructor. Color label. Label name editing', () => { describe(`Testing case "${caseId}"`, () => { it('To add multiple labels with a color change.', () => { - cy.addNewLabel(colorRed, labelAdditionalAttrs, labelColor.redHex); - cy.addNewLabel(colorGreen, labelAdditionalAttrs, labelColor.greenHex); - cy.addNewLabel(colorBlue, labelAdditionalAttrs, labelColor.blueHex); + cy.addNewLabel({ name: colorRed, color: labelColor.redHex }, labelAdditionalAttrs); + cy.addNewLabel({ name: colorGreen, color: labelColor.greenHex }, labelAdditionalAttrs); + cy.addNewLabel({ name: colorBlue, color: labelColor.blueHex }, labelAdditionalAttrs); }); it('Check color for created labels.', () => { @@ -130,7 +131,7 @@ context('Label constructor. Color label. Label name editing', () => { cy.goToTaskList(); cy.openTask(taskName); // Adding a label without setting a color - cy.addNewLabel(`Case ${caseId}`); + cy.addNewLabel({ name: `Case ${caseId}` }); cy.get('.cvat-constructor-viewer').should('be.visible'); cy.contains('.cvat-constructor-viewer-item', `Case ${caseId}`) .invoke('attr', 'style') diff --git a/tests/cypress/e2e/actions_tasks2/case_40_create_task_without_necessary_arguments.js b/tests/cypress/e2e/actions_tasks2/case_40_create_task_without_necessary_arguments.js index 652216dd688b..2c9d9eb6ad9b 100644 --- a/tests/cypress/e2e/actions_tasks2/case_40_create_task_without_necessary_arguments.js +++ b/tests/cypress/e2e/actions_tasks2/case_40_create_task_without_necessary_arguments.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -49,7 +50,7 @@ context('Try to create a task without necessary arguments.', () => { }); it('Input task labels. A task is not created.', () => { - cy.addNewLabel(labelName); + cy.addNewLabel({ name: labelName }); cy.contains('button', 'Submit & Continue').click(); cy.get('.cvat-notification-create-task-fail').should('exist'); cy.closeNotification('.cvat-notification-create-task-fail'); diff --git a/tests/cypress/e2e/actions_tasks2/case_76_try_create_task_incorrect_dataset_repo.js b/tests/cypress/e2e/actions_tasks2/case_76_try_create_task_incorrect_dataset_repo.js index 6c6827a12329..4812e779f191 100644 --- a/tests/cypress/e2e/actions_tasks2/case_76_try_create_task_incorrect_dataset_repo.js +++ b/tests/cypress/e2e/actions_tasks2/case_76_try_create_task_incorrect_dataset_repo.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -35,7 +36,7 @@ context('Try to create a task with an incorrect dataset repository.', () => { describe(`Testing "${labelName}"`, () => { it('Try create task with incorrect dataset repo URL.', () => { cy.get('[id="name"]').type(taskName); - cy.addNewLabel(labelName); + cy.addNewLabel({ name: labelName }); cy.get('input[type="file"]').attachFile(archiveName, { subjectType: 'drag-n-drop' }); cy.contains('.cvat-title', 'Advanced configuration').click(); cy.get('#repository').type(incorrectDatasetRepoUrl); diff --git a/tests/cypress/e2e/actions_tasks2/case_97_export_import_task.js b/tests/cypress/e2e/actions_tasks2/case_97_export_import_task.js index 1f7c2efe9d7e..d855f70a6903 100644 --- a/tests/cypress/e2e/actions_tasks2/case_97_export_import_task.js +++ b/tests/cypress/e2e/actions_tasks2/case_97_export_import_task.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -47,7 +48,7 @@ context('Export, import an annotation task.', { browser: '!firefox' }, () => { const [link] = url.split('?'); taskId = Number(link.split('/').slice(-1)[0]); }); - cy.addNewLabel(newLabelName); + cy.addNewLabel({ name: newLabelName }); cy.openJob(); cy.createRectangle(createRectangleShape2Points).then(() => { Cypress.config('scrollBehavior', false); diff --git a/tests/cypress/e2e/actions_tasks2/test_annotations_export_hash.js b/tests/cypress/e2e/actions_tasks2/test_annotations_export_hash.js new file mode 100644 index 000000000000..9fb53daf6b2f --- /dev/null +++ b/tests/cypress/e2e/actions_tasks2/test_annotations_export_hash.js @@ -0,0 +1,205 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +/// + +context('Test export hash when saving annotations', () => { + const taskName = 'Test export hash when saving annotations'; + const serverFiles = { + images: ['image_1.jpg', 'image_2.jpg', 'image_3.jpg'], + }; + + const generalLabel = { + name: 'test label', + }; + + const skeletonLabel = { + name: 'skeleton', + points: [ + { x: 0.55, y: 0.15 }, + { x: 0.20, y: 0.35 }, + { x: 0.43, y: 0.55 }, + { x: 0.63, y: 0.38 }, + { x: 0.27, y: 0.15 }, + ], + }; + + let taskID = null; + let jobID = null; + + before(() => { + cy.headlessLogin(); + cy.visit('/tasks/create'); + cy.get('#name').type(taskName); + cy.addNewLabel({ name: generalLabel.name }); + cy.addNewSkeletonLabel(skeletonLabel); + cy.selectFilesFromShare(serverFiles); + cy.intercept('POST', '/api/tasks**').as('createTaskRequest'); + cy.intercept('GET', '/api/jobs**').as('getJobsRequest'); + cy.contains('button', 'Submit & Continue').click(); + cy.wait('@createTaskRequest').then((interception) => { + expect(interception.response.statusCode).to.equal(201); + taskID = interception.response.body.id; + }); + cy.wait('@getJobsRequest').then((interception) => { + expect(interception.response.statusCode).to.equal(200); + jobID = interception.response.body.results[0].id; + }); + }); + + describe('Check saving twice', () => { + it('Saving twice different shapes', () => { + cy.visit(`/tasks/${taskID}/jobs/${jobID}`); + cy.get('.cvat-canvas-container').should('exist').and('be.visible'); + + cy.createRectangle({ + points: 'By 2 Points', + type: 'Shape', + labelName: generalLabel.name, + firstX: 150, + firstY: 350, + secondX: 200, + secondY: 400, + }); + + cy.createRectangle({ + points: 'By 2 Points', + type: 'Track', + labelName: generalLabel.name, + firstX: 200, + firstY: 400, + secondX: 250, + secondY: 450, + }); + + cy.createEllipse({ + type: 'Shape', + labelName: generalLabel.name, + firstX: 150, + firstY: 350, + secondX: 200, + secondY: 400, + }); + + cy.createEllipse({ + type: 'Track', + labelName: generalLabel.name, + firstX: 200, + firstY: 400, + secondX: 250, + secondY: 450, + }); + + cy.createPolygon({ + reDraw: false, + type: 'Shape', + labelName: generalLabel.name, + pointsMap: [ + { x: 200, y: 350 }, + { x: 250, y: 350 }, + { x: 250, y: 400 }, + { x: 200, y: 400 }, + ], + complete: true, + numberOfPoints: null, + }); + + cy.createPolygon({ + reDraw: false, + type: 'Track', + labelName: generalLabel.name, + pointsMap: [ + { x: 150, y: 400 }, + { x: 200, y: 400 }, + { x: 200, y: 450 }, + { x: 150, y: 450 }, + ], + complete: true, + numberOfPoints: null, + }); + + cy.createSkeleton({ + xtl: 150, + ytl: 350, + xbr: 200, + ybr: 400, + labelName: skeletonLabel.name, + type: 'Shape', + }); + + cy.createSkeleton({ + xtl: 200, + ytl: 400, + xbr: 250, + ybr: 450, + labelName: skeletonLabel.name, + type: 'Track', + }); + + cy.createTag(generalLabel.name); + + cy.intercept('PATCH', `/api/jobs/${jobID}/annotations**action=create**`).as('createJobAnnotations'); + cy.intercept('PATCH', `/api/jobs/${jobID}/annotations**action=update`).as('updateJobAnnotations'); + cy.intercept('PATCH', `/api/jobs/${jobID}/annotations**action=delete`).as('deleteJobAnnotations'); + + cy.saveJob(); + cy.wait('@createJobAnnotations').then((interception) => { + const { shapes, tags, tracks } = interception.response.body; + expect(tracks.length).to.be.equal(4); + expect(shapes.length).to.be.equal(4); + expect(tags.length).to.be.equal(1); + }); + + cy.wait('@updateJobAnnotations').then((interception) => { + const { shapes, tags, tracks } = interception.response.body; + expect(tracks.length).to.be.equal(0); + expect(shapes.length).to.be.equal(0); + expect(tags.length).to.be.equal(0); + }); + + cy.wait('@deleteJobAnnotations').then((interception) => { + const { shapes, tags, tracks } = interception.response.body; + expect(tracks.length).to.be.equal(0); + expect(shapes.length).to.be.equal(0); + expect(tags.length).to.be.equal(0); + }); + + cy.saveJob(); + cy.wait('@createJobAnnotations').then((interception) => { + const { shapes, tags, tracks } = interception.response.body; + expect(tracks.length).to.be.equal(0); + expect(shapes.length).to.be.equal(0); + expect(tags.length).to.be.equal(0); + }); + + cy.wait('@updateJobAnnotations').then((interception) => { + const { shapes, tags, tracks } = interception.response.body; + expect(tracks.length).to.be.equal(0); + expect(shapes.length).to.be.equal(0); + expect(tags.length).to.be.equal(0); + }); + + cy.wait('@deleteJobAnnotations').then((interception) => { + const { shapes, tags, tracks } = interception.response.body; + expect(tracks.length).to.be.equal(0); + expect(shapes.length).to.be.equal(0); + expect(tags.length).to.be.equal(0); + }); + }); + }); + + after(() => { + 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/actions_tasks2/test_default_attribute.js b/tests/cypress/e2e/actions_tasks2/test_default_attribute.js new file mode 100644 index 000000000000..49bd96946097 --- /dev/null +++ b/tests/cypress/e2e/actions_tasks2/test_default_attribute.js @@ -0,0 +1,115 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +/// + +context('Test default value for an attribute', () => { + const taskName = 'Default attribute value test'; + const serverFiles = { + images: ['image_1.jpg', 'image_2.jpg', 'image_3.jpg'], + }; + + const label = 'test label'; + const attributes = [ + { + name: 'test select attribute', + values: 'first{Enter}second{Enter}third', + defaultValue: 'second', + type: 'Select', + }, + { + name: 'test radio attribute', + values: 'first{Enter}second{Enter}third', + defaultValue: 'second', + type: 'Radio', + }, + ]; + + let taskID = null; + let jobID = null; + + function checkCreatedObject(attr1Value, attr2Value) { + cy.createRectangle({ + points: 'By 2 Points', + type: 'Shape', + labelName: label, + firstX: 150, + firstY: 350, + secondX: 250, + secondY: 450, + }); + + cy.get('#cvat-objects-sidebar-state-item-1').within(() => { + cy.contains('.cvat-objects-sidebar-state-item-collapse', 'DETAILS').click(); + cy.get('.cvat-object-item-select-attribute').contains(attr1Value); + cy.get('.cvat-object-item-radio-attribute').within(() => { + cy.get('.ant-radio-wrapper-checked').should('have.text', attr2Value); + }); + }); + } + + before(() => { + cy.headlessLogin(); + cy.visit('/tasks/create'); + cy.get('#name').type(taskName); + cy.addNewLabel({ name: label }, attributes); + cy.selectFilesFromShare(serverFiles); + cy.intercept('POST', '/api/tasks**').as('createTaskRequest'); + cy.intercept('GET', '/api/jobs**').as('getJobsRequest'); + cy.contains('button', 'Submit & Continue').click(); + cy.wait('@createTaskRequest').then((interception) => { + expect(interception.response.statusCode).to.equal(201); + taskID = interception.response.body.id; + }); + cy.wait('@getJobsRequest').then((interception) => { + expect(interception.response.statusCode).to.equal(200); + jobID = interception.response.body.results[0].id; + }); + }); + + describe('Annotation view has correct default attribute after task creationg', () => { + it('Rectangle has correct default attributes', () => { + cy.visit(`/tasks/${taskID}/jobs/${jobID}`); + cy.get('.cvat-canvas-container').should('exist').and('be.visible'); + checkCreatedObject(attributes[0].defaultValue, attributes[1].defaultValue); + }); + }); + + describe('Test can change default attribute', () => { + it('Can change default attribute value on task page', () => { + const newDefaultValue = 'third'; + cy.visit(`/tasks/${taskID}`); + cy.get('.cvat-task-details').should('exist').and('be.visible'); + cy.get('.cvat-constructor-viewer-item').within(() => { + cy.get('[aria-label="edit"]').click(); + }); + + cy.get('.cvat-attribute-inputs-wrapper').then(($el) => { + $el.each((_, el) => { + cy.wrap(el).within(() => { + cy.get('.ant-tag').contains(newDefaultValue).click(); + }); + }); + }); + cy.get('button[type="submit"]').click(); + cy.visit(`/tasks/${taskID}/jobs/${jobID}`); + cy.get('.cvat-canvas-container').should('exist').and('be.visible'); + checkCreatedObject(newDefaultValue, newDefaultValue); + }); + }); + + after(() => { + 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/actions_tasks3/case_107_connected_file_share.js b/tests/cypress/e2e/actions_tasks3/case_107_connected_file_share.js index 36d5367a79a7..e910e9bd39d0 100644 --- a/tests/cypress/e2e/actions_tasks3/case_107_connected_file_share.js +++ b/tests/cypress/e2e/actions_tasks3/case_107_connected_file_share.js @@ -9,46 +9,16 @@ context('Connected file share.', () => { const caseId = '107'; const taskName = `Case ${caseId}`; const labelName = taskName; - const expectedTopLevel = [ - { name: 'images', type: 'DIR', mime_type: 'DIR' }, - ]; - - const expectedImagesList = [ - { name: 'image_1.jpg', type: 'REG', mime_type: 'image' }, - { name: 'image_2.jpg', type: 'REG', mime_type: 'image' }, - { name: 'image_3.jpg', type: 'REG', mime_type: 'image' }, - ]; + const imageFiles = { + images: ['image_1.jpg', 'image_2.jpg', 'image_3.jpg'], + }; function createOpenTaskWithShare() { cy.get('.cvat-create-task-dropdown').click(); cy.get('.cvat-create-task-button').should('be.visible').click(); cy.get('#name').type(taskName); - cy.addNewLabel(labelName); - cy.intercept('GET', '/api/server/share?**').as('shareRequest'); - cy.contains('[role="tab"]', 'Connected file share').click(); - cy.wait('@shareRequest').then((interception) => { - for (const item of expectedTopLevel) { - const responseEl = interception.response.body.find((el) => el.name === item.name); - expect(responseEl).to.deep.equal(item); - } - }); - cy.get('.cvat-remote-browser-table-wrapper') - .should('exist') - .within(() => { - cy.get('button').contains('images').click(); - cy.wait('@shareRequest').then((interception) => { - expect(interception.response.body - .sort((a, b) => a.name.localeCompare(b.name)) - .filter((el) => el.mime_type === 'image')) - .to.deep.equal(expectedImagesList); - }); - expectedImagesList.forEach((el) => { - const { name } = el; - cy.get('.ant-table-cell').contains(name).parent().within(() => { - cy.get('.ant-checkbox-input').click(); - }); - }); - }); + cy.addNewLabel({ name: labelName }); + cy.selectFilesFromShare(imageFiles); cy.contains('button', 'Submit & Open').click(); cy.get('.cvat-task-details').should('exist'); } @@ -68,9 +38,8 @@ context('Connected file share.', () => { createOpenTaskWithShare(); cy.openJob(); cy.get('.cvat-player-filename-wrapper').then((playerFilenameWrapper) => { - for (let frame = 0; frame < expectedImagesList.length; frame++) { - const { name } = expectedImagesList[frame]; - cy.get(playerFilenameWrapper).should('have.text', `${expectedTopLevel[0].name}/${name}`); + for (let frame = 0; frame < imageFiles.images.length; frame++) { + cy.get(playerFilenameWrapper).should('have.text', `images/${imageFiles.images[frame]}`); cy.checkFrameNum(frame); cy.get('.cvat-player-next-button').click().trigger('mouseout'); } diff --git a/tests/cypress/e2e/actions_tasks3/case_118_multi_tasks.js b/tests/cypress/e2e/actions_tasks3/case_118_multi_tasks.js index b4358b05c856..7c8b00f6deb0 100644 --- a/tests/cypress/e2e/actions_tasks3/case_118_multi_tasks.js +++ b/tests/cypress/e2e/actions_tasks3/case_118_multi_tasks.js @@ -10,34 +10,25 @@ context('Create mutli tasks.', () => { const labelName = taskName; const sharePath = 'mounted_file_share'; - const expectedTopLevel = [ - { name: 'images', type: 'DIR', mime_type: 'DIR' }, - { name: 'videos', type: 'DIR', mime_type: 'DIR' }, - ]; - - const expectedImagesList = [ - { name: 'image_1.jpg', type: 'REG', mime_type: 'image' }, - { name: 'image_2.jpg', type: 'REG', mime_type: 'image' }, - { name: 'image_3.jpg', type: 'REG', mime_type: 'image' }, - ]; - - const expectedVideosList = [ - { name: 'video_1.mp4', type: 'REG', mime_type: 'video' }, - { name: 'video_2.mp4', type: 'REG', mime_type: 'video' }, - { name: 'video_3.mp4', type: 'REG', mime_type: 'video' }, - ]; + const imageFiles = { + images: ['image_1.jpg', 'image_2.jpg', 'image_3.jpg'], + }; + + const videoFiles = { + videos: ['video_1.mp4', 'video_2.mp4', 'video_3.mp4'], + }; function submitTask() { cy.get('.cvat-create-task-content-alert').should('not.exist'); cy.get('.cvat-create-task-content-footer [type="submit"]') .should('not.be.disabled') - .contains(`Submit ${expectedVideosList.length} tasks`) + .contains(`Submit ${videoFiles.videos.length} tasks`) .click(); } function checkCreatedTasks() { cy.get('.cvat-create-multi-tasks-progress', { timeout: 50000 }).should('exist') - .contains(`Total: ${expectedVideosList.length}`); + .contains(`Total: ${videoFiles.videos.length}`); cy.contains('button', 'Cancel'); cy.get('.cvat-create-multi-tasks-state').should('exist') .contains('Finished'); @@ -46,8 +37,8 @@ context('Create mutli tasks.', () => { }); cy.contains('button', 'Retry failed tasks').should('be.disabled'); cy.contains('button', 'Ok').click(); - expectedVideosList.forEach((video) => { - cy.contains('strong', video.name).should('exist'); + videoFiles.videos.forEach((video) => { + cy.contains('strong', video).should('exist'); }); } @@ -59,7 +50,7 @@ context('Create mutli tasks.', () => { beforeEach(() => { cy.get('.cvat-create-task-dropdown').click(); cy.get('.cvat-create-multi-tasks-button').should('be.visible').click(); - cy.addNewLabel(labelName); + cy.addNewLabel({ name: labelName }); }); afterEach(() => { @@ -73,10 +64,7 @@ context('Create mutli tasks.', () => { it('Trying to create a tasks with local images', () => { cy.contains('[role="tab"]', 'My computer').click(); - - const imageNames = expectedImagesList.map((image) => image.name); - const imagePaths = imageNames.map((name) => `${sharePath}/images/${name}`); - + const imagePaths = imageFiles.images.map((name) => `${sharePath}/images/${name}`); cy.get('input[type="file"]') .selectFile(imagePaths, { action: 'drag-drop', force: true }); @@ -86,32 +74,7 @@ context('Create mutli tasks.', () => { }); it('Trying to create a tasks with images from the shared storage', () => { - cy.intercept('GET', '/api/server/share?**').as('shareRequest'); - cy.contains('[role="tab"]', 'Connected file share').click(); - cy.wait('@shareRequest').then((interception) => { - for (const item of expectedTopLevel) { - const responseEl = interception.response.body.find((el) => el.name === item.name); - expect(responseEl).to.deep.equal(item); - } - }); - cy.get('.cvat-remote-browser-table-wrapper') - .should('exist') - .within(() => { - cy.get('button').contains('images').click(); - cy.wait('@shareRequest').then((interception) => { - expect(interception.response.body - .sort((a, b) => a.name.localeCompare(b.name)) - .filter((el) => el.mime_type === 'image')) - .to.deep.equal(expectedImagesList); - }); - expectedImagesList.forEach((el) => { - const { name } = el; - cy.get('.ant-table-cell').contains(name).parent().within(() => { - cy.get('.ant-checkbox-input').click(); - }); - }); - }); - + cy.selectFilesFromShare(imageFiles); cy.get('.cvat-create-task-content-alert').should('be.visible'); cy.get('.cvat-create-task-content-footer [type="submit"]').should('be.disabled'); }); @@ -129,10 +92,7 @@ context('Create mutli tasks.', () => { it('Trying to create a tasks with local videos', () => { cy.contains('[role="tab"]', 'My computer').click(); - - const videoNames = expectedVideosList.map((video) => video.name); - const videoPaths = videoNames.map((name) => `${sharePath}/videos/${name}`); - + const videoPaths = videoFiles.videos.map((name) => `${sharePath}/videos/${name}`); cy.get('input[type="file"]') .selectFile(videoPaths, { action: 'drag-drop', force: true }); @@ -143,31 +103,7 @@ context('Create mutli tasks.', () => { }); it('Trying to create a tasks with videos from the shared storage', () => { - cy.intercept('GET', '/api/server/share?**').as('shareRequest'); - cy.contains('[role="tab"]', 'Connected file share').click(); - cy.wait('@shareRequest').then((interception) => { - for (const item of expectedTopLevel) { - const responseEl = interception.response.body.find((el) => el.name === item.name); - expect(responseEl).to.deep.equal(item); - } - }); - cy.get('.cvat-remote-browser-table-wrapper') - .should('exist') - .within(() => { - cy.get('button').contains('videos').click(); - cy.wait('@shareRequest').then((interception) => { - expect(interception.response.body - .sort((a, b) => a.name.localeCompare(b.name))) - .to.deep.equal(expectedVideosList); - }); - expectedVideosList.forEach((el) => { - const { name } = el; - cy.get('.ant-table-cell').contains(name).parent().within(() => { - cy.get('.ant-checkbox-input').click(); - }); - }); - }); - + cy.selectFilesFromShare(videoFiles); submitTask(); checkCreatedTasks(); }); @@ -178,8 +114,8 @@ context('Create mutli tasks.', () => { const folder = 'tests/mounted_file_share'; cy.contains('[role="tab"]', 'Remote sources').click(); - expectedVideosList.forEach((video) => { - const URL = `${baseUrl}/${revision}/${folder}/${expectedTopLevel[1].name}/${video.name}`; + videoFiles.videos.forEach((video) => { + const URL = `${baseUrl}/${revision}/${folder}/videos/${video}`; cy.get('.cvat-file-selector-remote').type(URL).type('{enter}'); }); @@ -191,7 +127,7 @@ context('Create mutli tasks.', () => { after(() => { cy.logout(); cy.getAuthKey().then((authKey) => { - cy.deleteTasks(authKey, expectedVideosList.map((video) => video.name)); + cy.deleteTasks(authKey, videoFiles.videos); }); }); }); diff --git a/tests/cypress/e2e/actions_tasks3/case_18_filters_functionality.js b/tests/cypress/e2e/actions_tasks3/case_18_filters_functionality.js index bc75e3e64d25..ab0810227a6e 100644 --- a/tests/cypress/e2e/actions_tasks3/case_18_filters_functionality.js +++ b/tests/cypress/e2e/actions_tasks3/case_18_filters_functionality.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -10,15 +11,15 @@ context('Filters functionality.', () => { const caseId = '18'; const labelShape = 'shape 3 points'; const additionalAttrsLabelShape = [ - { additionalAttrName: 'type', additionalValue: 'shape', typeAttribute: 'Text' }, - { additionalAttrName: 'count points', additionalValue: '3', typeAttribute: 'Text' }, - { additionalAttrName: 'polygon', additionalValue: 'True', typeAttribute: 'Checkbox' }, + { name: 'type', values: 'shape', type: 'Text' }, + { name: 'count points', values: '3', type: 'Text' }, + { name: 'polygon', values: 'True', type: 'Checkbox' }, ]; const labelTrack = 'track 4 points'; const additionalAttrsLabelTrack = [ - { additionalAttrName: 'type', additionalValue: 'track', typeAttribute: 'Text' }, - { additionalAttrName: 'polygon', additionalValue: 'True', typeAttribute: 'Checkbox' }, - { additionalAttrName: 'count points', additionalValue: '4', typeAttribute: 'Text' }, + { name: 'type', values: 'track', type: 'Text' }, + { name: 'polygon', values: 'True', type: 'Checkbox' }, + { name: 'count points', values: '4', type: 'Text' }, ]; const createPolygonShape = { @@ -70,10 +71,10 @@ context('Filters functionality.', () => { const createEllipseTrack = { type: 'Track', labelName: labelTrack, - cx: 250, - cy: 350, - rightX: 450, - topY: 280, + firstX: 250, + firstY: 350, + secondX: 450, + secondY: 280, }; const cvatCanvasShapeList = []; @@ -93,8 +94,8 @@ context('Filters functionality.', () => { before(() => { cy.openTask(taskName); - cy.addNewLabel(labelShape, additionalAttrsLabelShape); - cy.addNewLabel(labelTrack, additionalAttrsLabelTrack); + cy.addNewLabel({ name: labelShape }, additionalAttrsLabelShape); + cy.addNewLabel({ name: labelTrack }, additionalAttrsLabelTrack); cy.openJob(); }); diff --git a/tests/cypress/e2e/actions_tasks3/case_1_create_delete_task_label_color.js b/tests/cypress/e2e/actions_tasks3/case_1_create_delete_task_label_color.js index cd2d7f2674dc..124db0fa3592 100644 --- a/tests/cypress/e2e/actions_tasks3/case_1_create_delete_task_label_color.js +++ b/tests/cypress/e2e/actions_tasks3/case_1_create_delete_task_label_color.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -37,7 +38,7 @@ context('Create and delete a annotation task. Color collision.', () => { it('Add a label. Check labels color.', () => { cy.openTask(taskName); - cy.addNewLabel(newLabelName); + cy.addNewLabel({ name: newLabelName }); cy.get('.cvat-constructor-viewer-item').first().then((firstLabel) => { cy.get('.cvat-constructor-viewer-item').last().then((secondLabel) => { expect(firstLabel.attr('style')).not.equal(secondLabel.attr('style')); diff --git a/tests/cypress/e2e/actions_tasks3/case_44_changing_default_value_for_attribute.js b/tests/cypress/e2e/actions_tasks3/case_44_changing_default_value_for_attribute.js index 5241e2b73fc6..fd641be141c0 100644 --- a/tests/cypress/e2e/actions_tasks3/case_44_changing_default_value_for_attribute.js +++ b/tests/cypress/e2e/actions_tasks3/case_44_changing_default_value_for_attribute.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -10,9 +11,9 @@ context('Changing a default value for an attribute.', () => { const caseId = '44'; const additionalLabel = `Case ${caseId}`; const additionalAttrsLabel = [ - { additionalAttrName: 'type', additionalValue: '', typeAttribute: 'Text' }, - { additionalAttrName: 'count', additionalValue: '0;5;1', typeAttribute: 'Number' }, // Check issue 2968 - { additionalAttrName: 'shape', additionalValue: 'False', typeAttribute: 'Checkbox' }, + { name: 'type', values: '', type: 'Text' }, + { name: 'count', values: '0;5;1', type: 'Number' }, // Check issue 2968 + { name: 'shape', values: 'False', type: 'Checkbox' }, ]; const rectangleShape2Points = { points: 'By 2 Points', @@ -34,7 +35,7 @@ context('Changing a default value for an attribute.', () => { describe(`Testing case "${caseId}", issue 2968`, () => { it('Add a label, add text (leave it’s value empty by default) & checkbox attributes.', () => { cy.intercept('PATCH', '/api/tasks/**').as('patchTask'); - cy.addNewLabel(additionalLabel, additionalAttrsLabel); + cy.addNewLabel({ name: additionalLabel }, additionalAttrsLabel); cy.wait('@patchTask').its('response.statusCode').should('equal', 200); cy.get('.cvat-constructor-viewer').should('exist').and('be.visible'); }); @@ -76,9 +77,9 @@ context('Changing a default value for an attribute.', () => { cy.createRectangle(rectangleShape2Points); cy.get('#cvat_canvas_shape_1').trigger('mousemove'); [ - [additionalAttrsLabel[0].additionalAttrName, newTextValue], - [additionalAttrsLabel[1].additionalAttrName, additionalAttrsLabel[1].additionalValue.split(';')[0]], - [additionalAttrsLabel[2].additionalAttrName, newCheckboxValue.toLowerCase()], + [additionalAttrsLabel[0].name, newTextValue], + [additionalAttrsLabel[1].name, additionalAttrsLabel[1].values.split(';')[0]], + [additionalAttrsLabel[2].name, newCheckboxValue.toLowerCase()], ].forEach(([attrName, attrValue]) => { // eslint-disable-next-line security/detect-non-literal-regexp cy.contains(new RegExp(`^${attrName}: ${attrValue}$`)).should('be.visible'); diff --git a/tests/cypress/e2e/actions_tasks3/case_46_create_task_with_files_from_remote_sources.js b/tests/cypress/e2e/actions_tasks3/case_46_create_task_with_files_from_remote_sources.js index f8d343c63e79..85b49f09e3ff 100644 --- a/tests/cypress/e2e/actions_tasks3/case_46_create_task_with_files_from_remote_sources.js +++ b/tests/cypress/e2e/actions_tasks3/case_46_create_task_with_files_from_remote_sources.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -27,7 +28,7 @@ context('Create a task with files from remote sources.', () => { describe(`Testing "${labelName}"`, () => { it('Try to create a task with wrong remote file. The task is not created.', () => { cy.get('[id="name"]').type(taskName); - cy.addNewLabel(labelName); + cy.addNewLabel({ name: labelName }); cy.contains('Remote sources').click(); cy.get('.cvat-file-selector-remote').type(wrongUrl); cy.contains('button', 'Submit & Continue').click(); diff --git a/tests/cypress/e2e/actions_tasks3/case_48_issue_2663_annotations_statistics.js b/tests/cypress/e2e/actions_tasks3/case_48_issue_2663_annotations_statistics.js index bb6ad4acdeed..a6e34bb26bd2 100644 --- a/tests/cypress/e2e/actions_tasks3/case_48_issue_2663_annotations_statistics.js +++ b/tests/cypress/e2e/actions_tasks3/case_48_issue_2663_annotations_statistics.js @@ -30,18 +30,18 @@ context('Annotations statistics.', () => { const createEllipseShape = { type: 'Shape', labelName, - cx: 400, - cy: 400, - rightX: 500, - topY: 350, + firstX: 400, + firstY: 400, + secondX: 500, + secondY: 350, }; const createEllipseTrack = { type: 'Track', labelName, - cx: createEllipseShape.cx, - cy: createEllipseShape.cy - 150, - rightX: createEllipseShape.rightX, - topY: createEllipseShape.topY - 150, + firstX: createEllipseShape.firstX, + firstY: createEllipseShape.firstY - 150, + secondX: createEllipseShape.secondX, + secondY: createEllipseShape.secondY - 150, }; const createCuboidShape2Points = { points: 'From rectangle', diff --git a/tests/cypress/e2e/canvas3d_functionality/case_89_canvas3d_functionality_filters.js b/tests/cypress/e2e/canvas3d_functionality/case_89_canvas3d_functionality_filters.js index 845ed367775a..b2f8eafa8c93 100644 --- a/tests/cypress/e2e/canvas3d_functionality/case_89_canvas3d_functionality_filters.js +++ b/tests/cypress/e2e/canvas3d_functionality/case_89_canvas3d_functionality_filters.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -25,7 +26,7 @@ context('Canvas 3D functionality. Filters.', () => { before(() => { cy.openTask(taskName); - cy.addNewLabel(secondLabel); + cy.addNewLabel({ name: secondLabel }); cy.openJob(); cy.wait(1000); // Waiting for the point cloud to display cy.create3DCuboid(firstCuboidCreationParams); diff --git a/tests/cypress/e2e/canvas3d_functionality_2/case_78_canvas3d_functionality_cuboid_label.js b/tests/cypress/e2e/canvas3d_functionality_2/case_78_canvas3d_functionality_cuboid_label.js index 1fc15e28f86c..f5a00fc404b9 100644 --- a/tests/cypress/e2e/canvas3d_functionality_2/case_78_canvas3d_functionality_cuboid_label.js +++ b/tests/cypress/e2e/canvas3d_functionality_2/case_78_canvas3d_functionality_cuboid_label.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -21,7 +22,7 @@ context('Canvas 3D functionality. Interaction with cuboid via sidebar.', () => { before(() => { cy.openTask(taskName); - cy.addNewLabel(secondLabel, secondLabelAdditionalAttrs, secondLabelColorRed); + cy.addNewLabel({ name: secondLabel, color: secondLabelColorRed }, secondLabelAdditionalAttrs); cy.openJob(); cy.wait(1000); // Waiting for the point cloud to display cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_before_all'); diff --git a/tests/cypress/e2e/canvas3d_functionality_2/case_80_canvas3d_functionality_cuboid_make_copy.js b/tests/cypress/e2e/canvas3d_functionality_2/case_80_canvas3d_functionality_cuboid_make_copy.js index 9df0eb07d3e1..d2ea214095a1 100644 --- a/tests/cypress/e2e/canvas3d_functionality_2/case_80_canvas3d_functionality_cuboid_make_copy.js +++ b/tests/cypress/e2e/canvas3d_functionality_2/case_80_canvas3d_functionality_cuboid_make_copy.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -18,7 +19,7 @@ context('Canvas 3D functionality. Make a copy.', () => { before(() => { cy.openTask(taskName); - cy.addNewLabel(secondLabel); + cy.addNewLabel({ name: secondLabel }); cy.openJob(); cy.wait(1000); // Waiting for the point cloud to display cy.create3DCuboid(cuboidCreationParams); diff --git a/tests/cypress/e2e/issues_prs/issue_1919_check_text_attr.js b/tests/cypress/e2e/issues_prs/issue_1919_check_text_attr.js index 44462446c1c1..abb6ea94f44a 100644 --- a/tests/cypress/e2e/issues_prs/issue_1919_check_text_attr.js +++ b/tests/cypress/e2e/issues_prs/issue_1919_check_text_attr.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 Intel Corporation // // SPDX-License-Identifier: MIT @@ -30,9 +31,11 @@ context('Check label attribute changes', () => { cy.createRectangle(createRectangleShape2Points); cy.get('#cvat_canvas_shape_1').trigger('mousemove').rightclick(); }); + it('Open object menu details', () => { cy.get('.cvat-canvas-context-menu').contains('DETAILS').click(); }); + it('Clear field of text attribute and write new value', () => { cy.get('.cvat-canvas-context-menu') .contains(attrName) @@ -44,6 +47,7 @@ context('Check label attribute changes', () => { .type(newLabelAttrValue); }); }); + it('Check what value of right panel is changed too', () => { cy.get('#cvat-objects-sidebar-state-item-1') .contains(attrName) @@ -52,5 +56,28 @@ context('Check label attribute changes', () => { cy.get('.cvat-object-item-text-attribute').should('have.value', newLabelAttrValue); }); }); + + it('Specify many lines for a text attribute, update the page and check values', () => { + const multilineValue = 'This text attributes has many lines.\n - Line 1\n - Line 2'; + cy.get('.cvat-canvas-context-menu') + .contains(attrName) + .parents('.cvat-object-item-attribute-wrapper') + .within(() => { + cy.get('.cvat-object-item-text-attribute') + .clear() + .type(multilineValue); + }); + cy.saveJob(); + cy.reload(); + cy.get('.cvat-canvas-container').should('exist').and('be.visible'); + cy.get('#cvat-objects-sidebar-state-item-1') + .contains('DETAILS').click(); + cy.get('#cvat-objects-sidebar-state-item-1') + .contains(attrName) + .parents('.cvat-object-item-attribute-wrapper') + .within(() => { + cy.get('.cvat-object-item-text-attribute').should('have.value', multilineValue); + }); + }); }); }); diff --git a/tests/cypress/e2e/issues_prs2/issue_1425_highlighted_attribute_correspond_chosen_attribute.js b/tests/cypress/e2e/issues_prs2/issue_1425_highlighted_attribute_correspond_chosen_attribute.js index 9727f8e6442b..6b67969f8193 100644 --- a/tests/cypress/e2e/issues_prs2/issue_1425_highlighted_attribute_correspond_chosen_attribute.js +++ b/tests/cypress/e2e/issues_prs2/issue_1425_highlighted_attribute_correspond_chosen_attribute.js @@ -38,7 +38,7 @@ context('The highlighted attribute in AAM should correspond to the chosen attrib }); }); cy.get('.cvat-attribute-annotation-sidebar-attr-editor').within(() => { - cy.get('[type="text"]').should('have.value', textValue); + cy.get('textarea').should('have.value', textValue); }); }); it('Go to next attribute and check again', () => { @@ -49,7 +49,7 @@ context('The highlighted attribute in AAM should correspond to the chosen attrib }); }); cy.get('.cvat-attribute-annotation-sidebar-attr-editor').within(() => { - cy.get('[type="text"]').should('have.value', textValue); + cy.get('textarea').should('have.value', textValue); }); }); }); diff --git a/tests/cypress/e2e/issues_prs2/issue_1429_check_new_label.js b/tests/cypress/e2e/issues_prs2/issue_1429_check_new_label.js index 1d630722d149..d8c68a996044 100644 --- a/tests/cypress/e2e/issues_prs2/issue_1429_check_new_label.js +++ b/tests/cypress/e2e/issues_prs2/issue_1429_check_new_label.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -29,7 +30,7 @@ context('Check if the new label reflects in the options', () => { cy.url().should('include', '/tasks').and('not.contain', '/jobs'); }); it('Add new label', () => { - cy.addNewLabel(newLabelName); + cy.addNewLabel({ name: newLabelName }); }); it('Open the job again', () => { cy.openJob(); diff --git a/tests/cypress/e2e/skeletons/skeletons_pipeline.js b/tests/cypress/e2e/skeletons/skeletons_pipeline.js index bff55bc3a8fa..69a2e79fba75 100644 --- a/tests/cypress/e2e/skeletons/skeletons_pipeline.js +++ b/tests/cypress/e2e/skeletons/skeletons_pipeline.js @@ -6,7 +6,16 @@ context('Manipulations with skeletons', { scrollBehavior: false }, () => { const skeletonSize = 5; - const labelName = 'skeleton'; + const skeleton = { + name: 'skeleton', + points: [ + { x: 0.55, y: 0.15 }, + { x: 0.20, y: 0.35 }, + { x: 0.43, y: 0.55 }, + { x: 0.63, y: 0.38 }, + { x: 0.27, y: 0.15 }, + ], + }; const taskName = 'skeletons main pipeline'; const imagesFolder = `cypress/fixtures/${taskName}`; const archiveName = `${taskName}.zip`; @@ -64,79 +73,18 @@ context('Manipulations with skeletons', { scrollBehavior: false }, () => { it('Create a simple task', () => { cy.visit('/tasks/create'); cy.get('#name').type(taskName); - cy.get('.cvat-constructor-viewer-new-skeleton-item').click(); - cy.get('.cvat-skeleton-configurator').should('exist').and('be.visible'); - - cy.get('.cvat-label-constructor-creator').within(() => { - cy.get('#name').type(labelName); - cy.get('.ant-radio-button-checked').within(() => { - cy.get('.ant-radio-button-input').should('have.attr', 'value', 'point'); - }); - }); - - const pointsOffset = [ - { x: 0.55, y: 0.15 }, - { x: 0.20, y: 0.35 }, - { x: 0.43, y: 0.55 }, - { x: 0.63, y: 0.38 }, - { x: 0.27, y: 0.15 }, - ]; - expect(skeletonSize).to.be.equal(pointsOffset.length); - - cy.get('.cvat-skeleton-configurator-svg').then(($canvas) => { - const canvas = $canvas[0]; - - canvas.scrollIntoView(); - const rect = canvas.getBoundingClientRect(); - const { width, height } = rect; - pointsOffset.forEach(({ x: xOffset, y: yOffset }) => { - canvas.dispatchEvent(new MouseEvent('mousedown', { - clientX: rect.x + width * xOffset, - clientY: rect.y + height * yOffset, - button: 0, - bubbles: true, - })); - }); - - cy.get('.ant-radio-button-wrapper:nth-child(3)').click().within(() => { - cy.get('.ant-radio-button-input').should('have.attr', 'value', 'join'); - }); - - cy.get('.cvat-skeleton-configurator-svg').within(() => { - cy.get('circle').then(($circles) => { - expect($circles.length).to.be.equal(5); - $circles.each(function (i) { - const circle1 = this; - $circles.each(function (j) { - const circle2 = this; - if (i === j) return; - circle1.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); - circle1.dispatchEvent(new MouseEvent('click', { button: 0, bubbles: true })); - circle1.dispatchEvent(new MouseEvent('mouseout', { bubbles: true })); - - circle2.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); - circle2.dispatchEvent(new MouseEvent('click', { button: 0, bubbles: true })); - circle2.dispatchEvent(new MouseEvent('mouseout', { bubbles: true })); - }); - }); - }); - }); - - cy.contains('Continue').scrollIntoView().click(); - cy.contains('Continue').scrollIntoView().click(); - cy.get('input[type="file"]').attachFile(archiveName, { subjectType: 'drag-n-drop' }); - - cy.intercept('/api/tasks?**').as('taskPost'); - cy.contains('Submit & Open').scrollIntoView().click(); - - cy.wait('@taskPost').then((interception) => { - taskID = interception.response.body.id; - expect(interception.response.statusCode).to.be.equal(201); - cy.intercept(`/api/tasks/${taskID}`).as('getTask'); - cy.wait('@getTask', { timeout: 10000 }); - cy.get('.cvat-job-item').should('exist').and('be.visible'); - cy.openJob(); - }); + cy.addNewSkeletonLabel(skeleton); + expect(skeletonSize).to.be.equal(skeleton.points.length); + cy.get('input[type="file"]').attachFile(archiveName, { subjectType: 'drag-n-drop' }); + cy.intercept('/api/tasks?**').as('taskPost'); + cy.contains('Submit & Open').scrollIntoView().click(); + cy.wait('@taskPost').then((interception) => { + taskID = interception.response.body.id; + expect(interception.response.statusCode).to.be.equal(201); + cy.intercept(`/api/tasks/${taskID}`).as('getTask'); + cy.wait('@getTask', { timeout: 10000 }); + cy.get('.cvat-job-item').should('exist').and('be.visible'); + cy.openJob(); }); }); }); @@ -145,7 +93,7 @@ context('Manipulations with skeletons', { scrollBehavior: false }, () => { function createSkeletonObject(shapeType) { cy.createSkeleton({ ...skeletonPosition, - labelName, + labelName: skeleton.name, type: `${shapeType[0].toUpperCase()}${shapeType.slice(1).toLowerCase()}`, }); cy.get('#cvat_canvas_shape_1').should('exist').and('be.visible'); diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 88fbf076ff84..0a95d342af2d 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -229,6 +229,43 @@ Cypress.Commands.add( }, ); +Cypress.Commands.add('selectFilesFromShare', (serverFiles) => { + cy.intercept('GET', '/api/server/share?**').as('shareRequest'); + cy.contains('[role="tab"]', 'Connected file share').click(); + cy.wait('@shareRequest'); + + const selectServerFiles = (files) => { + if (Array.isArray(files)) { + cy.get('.cvat-remote-browser-table-wrapper').within(() => { + files.forEach((file) => { + cy.get('.ant-table-cell').contains(file).parent().within(() => { + cy.get('.ant-checkbox-input').click(); + }); + }); + }); + cy.get('.cvat-remote-browser-nav-breadcrumb').contains('root').click(); + } else { + for (const directory of Object.keys(files)) { + cy.get('.cvat-remote-browser-table-wrapper').within(() => { + cy.get('button').contains(directory).click(); + cy.wait('@shareRequest'); + }); + selectServerFiles(files[directory]); + } + } + }; + + selectServerFiles(serverFiles); +}); + +Cypress.Commands.add('headlessLogin', (username = Cypress.env('user'), password = Cypress.env('password')) => { + cy.visit('/'); + cy.get('#root').should('exist').and('be.visible'); + cy.window().then(async ($win) => { + await $win.cvat.server.login(username, password); + }); +}); + Cypress.Commands.add('headlessCreateTask', (taskSpec, dataSpec) => { cy.window().then(async ($win) => { const task = new $win.cvat.classes.Task({ @@ -239,6 +276,7 @@ Cypress.Commands.add('headlessCreateTask', (taskSpec, dataSpec) => { if (dataSpec.server_files) { task.serverFiles = dataSpec.server_files; } + if (dataSpec.client_files) { task.clientFiles = dataSpec.client_files; } @@ -455,8 +493,8 @@ Cypress.Commands.add('createEllipse', (createEllipseParams) => { cy.contains('button', createEllipseParams.type).click(); }); cy.get('.cvat-canvas-container') - .click(createEllipseParams.cx, createEllipseParams.cy) - .click(createEllipseParams.rightX, createEllipseParams.topY); + .click(createEllipseParams.firstX, createEllipseParams.firstY) + .click(createEllipseParams.secondX, createEllipseParams.secondY); cy.checkPopoverHidden('draw-ellipse'); cy.checkObjectParameters(createEllipseParams, 'ELLIPSE'); }); @@ -526,7 +564,7 @@ Cypress.Commands.add('createPolygon', (createPolygonParams) => { Cypress.Commands.add('openSettings', () => { cy.get('.cvat-right-header').find('.cvat-header-menu-user-dropdown').trigger('mouseover', { which: 1 }); - cy.get('.anticon-setting').click(); + cy.get('.anticon-setting').should('exist').and('be.visible').click(); cy.get('.cvat-settings-modal').should('be.visible'); }); @@ -589,7 +627,7 @@ Cypress.Commands.add('createCuboid', (createCuboidParams) => { cy.checkObjectParameters(createCuboidParams, 'CUBOID'); }); -Cypress.Commands.add('updateAttributes', (multiAttrParams) => { +Cypress.Commands.add('updateAttributes', (attributes) => { const cvatAttributeInputsWrapperId = []; cy.get('.cvat-new-attribute-button').click(); cy.document().then((doc) => { @@ -601,29 +639,37 @@ Cypress.Commands.add('updateAttributes', (multiAttrParams) => { const minId = Math.min(...cvatAttributeInputsWrapperId); cy.get(`[cvat-attribute-id="${minId}"]`).within(() => { - cy.get('.cvat-attribute-name-input').type(multiAttrParams.additionalAttrName); + cy.get('.cvat-attribute-name-input').type(attributes.name); cy.get('.cvat-attribute-type-input').click(); }); - cy.get('.ant-select-dropdown') + cy.get('.ant-select-dropdown:has(.cvat-attribute-type-input-select)') .not('.ant-select-dropdown-hidden') + .should('exist').and('be.visible') .first() .within(() => { - cy.get(`.ant-select-item-option[title="${multiAttrParams.typeAttribute}"]`).click(); + cy.get(`.cvat-attribute-type-input-${attributes.type.toLowerCase()}`).click(); }); - if (multiAttrParams.typeAttribute === 'Text' || multiAttrParams.typeAttribute === 'Number') { + if (['Number', 'Text'].includes(attributes.type)) { cy.get(`[cvat-attribute-id="${minId}"]`).within(() => { - if (multiAttrParams.additionalValue !== '') { - cy.get('.cvat-attribute-values-input').type(multiAttrParams.additionalValue); + if (attributes.values !== '') { + cy.get('.cvat-attribute-values-input').type(attributes.values); } else { cy.get('.cvat-attribute-values-input').clear(); } }); - } else if (multiAttrParams.typeAttribute === 'Radio') { + } else if (['Radio', 'Select'].includes(attributes.type)) { cy.get(`[cvat-attribute-id="${minId}"]`).within(() => { - cy.get('.cvat-attribute-values-input').type(`${multiAttrParams.additionalValue}{Enter}`); + cy.get('.cvat-attribute-values-input').type(`${attributes.values}{Enter}`); + + if (attributes.defaultValue) { + cy.get('.cvat-attribute-values-input').within(() => { + cy.get('.ant-tag').contains(attributes.defaultValue).click({ force: true }); + cy.get('.ant-tag').should('have.class', 'ant-tag-blue'); + }); + } }); - } else if (multiAttrParams.typeAttribute === 'Checkbox') { + } else if (attributes.type === 'Checkbox') { cy.get(`[cvat-attribute-id="${minId}"]`).within(() => { cy.get('.cvat-attribute-values-input').click(); }); @@ -631,10 +677,10 @@ Cypress.Commands.add('updateAttributes', (multiAttrParams) => { .not('.ant-select-dropdown-hidden') .first() .within(() => { - cy.get(`.ant-select-item-option[title="${multiAttrParams.additionalValue}"]`).click(); + cy.get(`.ant-select-item-option[title="${attributes.values}"]`).click(); }); } - if (multiAttrParams.mutable) { + if (attributes.mutable) { cy.get('.cvat-attribute-mutable-checkbox') .find('[type="checkbox"]') .should('not.be.checked') @@ -862,17 +908,17 @@ Cypress.Commands.add('deleteLabel', (labelName) => { cy.contains('.cvat-constructor-viewer-item', new RegExp(`^${labelName}$`)).should('not.exist'); }); -Cypress.Commands.add('addNewLabel', (newLabelName, additionalAttrs, labelColor) => { +Cypress.Commands.add('addNewLabel', ({ name, color }, additionalAttrs) => { cy.collectLabelsName().then((labelsNames) => { - if (labelsNames.includes(newLabelName)) { - cy.deleteLabel(newLabelName); + if (labelsNames.includes(name)) { + cy.deleteLabel(name); } }); cy.contains('button', 'Add label').click(); - cy.get('[placeholder="Label name"]').type(newLabelName); - if (labelColor) { + cy.get('[placeholder="Label name"]').type(name); + if (color) { cy.get('.cvat-change-task-label-color-badge').click(); - cy.changeColorViaBadge(labelColor); + cy.changeColorViaBadge(color); } if (additionalAttrs) { for (let i = 0; i < additionalAttrs.length; i++) { @@ -883,7 +929,61 @@ Cypress.Commands.add('addNewLabel', (newLabelName, additionalAttrs, labelColor) cy.contains('button', 'Cancel').click(); cy.get('.cvat-spinner').should('not.exist'); cy.get('.cvat-constructor-viewer').should('be.visible'); - cy.contains('.cvat-constructor-viewer-item', new RegExp(`^${newLabelName}$`)).should('exist'); + cy.contains('.cvat-constructor-viewer-item', new RegExp(`^${name}$`)).should('exist'); +}); + +Cypress.Commands.add('addNewSkeletonLabel', ({ name, points }) => { + cy.get('.cvat-constructor-viewer-new-skeleton-item').click(); + cy.get('.cvat-skeleton-configurator').should('exist').and('be.visible'); + + cy.get('.cvat-label-constructor-creator').within(() => { + cy.get('#name').type(name); + cy.get('.ant-radio-button-checked').within(() => { + cy.get('.ant-radio-button-input').should('have.attr', 'value', 'point'); + }); + }); + + cy.get('.cvat-skeleton-configurator-svg').then(($canvas) => { + const canvas = $canvas[0]; + canvas.scrollIntoView(); + const rect = canvas.getBoundingClientRect(); + const { width, height } = rect; + points.forEach(({ x: xOffset, y: yOffset }) => { + canvas.dispatchEvent(new MouseEvent('mousedown', { + clientX: rect.x + width * xOffset, + clientY: rect.y + height * yOffset, + button: 0, + bubbles: true, + })); + }); + + cy.get('.ant-radio-button-wrapper:nth-child(3)').click().within(() => { + cy.get('.ant-radio-button-input').should('have.attr', 'value', 'join'); + }); + + cy.get('.cvat-skeleton-configurator-svg').within(() => { + cy.get('circle').then(($circles) => { + expect($circles.length).to.be.equal(5); + $circles.each(function (i) { + const circle1 = this; + $circles.each(function (j) { + const circle2 = this; + if (i === j) return; + circle1.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); + circle1.dispatchEvent(new MouseEvent('click', { button: 0, bubbles: true })); + circle1.dispatchEvent(new MouseEvent('mouseout', { bubbles: true })); + + circle2.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); + circle2.dispatchEvent(new MouseEvent('click', { button: 0, bubbles: true })); + circle2.dispatchEvent(new MouseEvent('mouseout', { bubbles: true })); + }); + }); + }); + }); + + cy.contains('Continue').scrollIntoView().click(); + cy.contains('Continue').scrollIntoView().click(); + }); }); Cypress.Commands.add('checkCanvasSidebarColorEqualness', (id) => { diff --git a/tests/cypress/support/const.js b/tests/cypress/support/const.js index dc2c1770edc9..09d5e9523157 100644 --- a/tests/cypress/support/const.js +++ b/tests/cypress/support/const.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -28,9 +29,9 @@ export const advancedConfigurationParams = { frameStep: 2, }; export const multiAttrParams = { - additionalAttrName: 'Attr 2', - additionalValue: 'Attr value 2', - typeAttribute: 'Text', + name: 'Attr 2', + values: 'Attr value 2', + type: 'Text', }; it('Prepare to testing', () => { diff --git a/tests/cypress/support/const_project.js b/tests/cypress/support/const_project.js index 93a6c1473f09..0178faa96e28 100644 --- a/tests/cypress/support/const_project.js +++ b/tests/cypress/support/const_project.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -9,9 +10,9 @@ export const labelName = `Base label for ${projectName}`; export const attrName = `Attr for ${labelName}`; export const textDefaultValue = 'Some default value for type Text'; export const multiAttrParams = { - additionalAttrName: 'Attr 2', - additionalValue: 'Attr value 2', - typeAttribute: 'Text', + name: 'Attr 2', + values: 'Attr value 2', + type: 'Text', }; it('Prepare to testing', () => { diff --git a/tests/json_to_html.py b/tests/json_to_html.py deleted file mode 100644 index 2ef756220939..000000000000 --- a/tests/json_to_html.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (C) 2021-2022 Intel Corporation -# -# SPDX-License-Identifier: MIT - -from json2html import json2html -import sys -import os -import json - -def json_to_html(path_to_json): - with open(path_to_json) as json_file: - data = json.load(json_file) - hadolint_html_report = json2html.convert(json = data) - - with open(os.path.splitext(path_to_json)[0] + '.html', 'w') as html_file: - html_file.write(hadolint_html_report) - - -if __name__ == '__main__': - json_to_html(sys.argv[1]) diff --git a/tests/python/cli/util.py b/tests/python/cli/util.py index 021d28082f1f..034d5d073ace 100644 --- a/tests/python/cli/util.py +++ b/tests/python/cli/util.py @@ -26,6 +26,6 @@ def generate_images(dst_dir: Path, count: int) -> List[Path]: dst_dir.mkdir(parents=True, exist_ok=True) for i in range(count): filename = dst_dir / f"img_{i}.jpg" - filename.write_bytes(generate_image_file().getvalue()) + filename.write_bytes(generate_image_file(filename.name).getvalue()) filenames.append(filename) return filenames diff --git a/tests/python/requirements.txt b/tests/python/requirements.txt index f1d7333f1926..79cdfc6a992e 100644 --- a/tests/python/requirements.txt +++ b/tests/python/requirements.txt @@ -1,7 +1,7 @@ pytest==6.2.5 pytest-cases==3.6.13 pytest-timeout==2.1.0 -pytest-cov==4.0.0 +pytest-cov==4.1.0 requests==2.31.0 deepdiff==5.6.0 boto3==1.17.61 diff --git a/tests/python/rest_api/test_analytics_reports.py b/tests/python/rest_api/test_analytics_reports.py new file mode 100644 index 000000000000..a50c053fc138 --- /dev/null +++ b/tests/python/rest_api/test_analytics_reports.py @@ -0,0 +1,379 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +import json +from http import HTTPStatus +from typing import Any, Dict, Optional + +import pytest +from cvat_sdk.api_client import models +from deepdiff import DeepDiff + +from shared.utils.config import make_api_client + + +class _PermissionTestBase: + @staticmethod + def _get_query_params( + job_id: Optional[int] = None, + task_id: Optional[int] = None, + project_id: Optional[int] = None, + ): + params = {} + if job_id is not None: + params["job_id"] = job_id + elif task_id is not None: + params["task_id"] = task_id + elif project_id is not None: + params["project_id"] = project_id + + return params + + def create_analytics_report( + self, + user: str, + *, + job_id: Optional[int] = None, + task_id: Optional[int] = None, + project_id: Optional[int] = None, + ): + params = self._get_query_params(job_id=job_id, task_id=task_id, project_id=project_id) + + with make_api_client(user) as api_client: + (_, response) = api_client.analytics_api.create_report( + analytics_report_create_request=models.AnalyticsReportCreateRequest(**params), + _parse_response=False, + ) + assert response.status == HTTPStatus.ACCEPTED + rq_id = json.loads(response.data)["rq_id"] + + while True: + (_, response) = api_client.analytics_api.create_report( + rq_id=rq_id, _parse_response=False + ) + assert response.status in [HTTPStatus.CREATED, HTTPStatus.ACCEPTED] + + if response.status == HTTPStatus.CREATED: + break + + +@pytest.mark.usefixtures("restore_db_per_class") +class TestGetAnalyticsReports(_PermissionTestBase): + def _test_get_report_200( + self, + user: str, + *, + job_id: Optional[int] = None, + task_id: Optional[int] = None, + project_id: Optional[int] = None, + expected_data: Optional[Dict[str, Any]] = None, + **kwargs, + ): + params = self._get_query_params(job_id=job_id, task_id=task_id, project_id=project_id) + with make_api_client(user) as api_client: + (_, response) = api_client.analytics_api.get_reports(**params, **kwargs) + assert response.status == HTTPStatus.OK + + if expected_data is not None: + assert DeepDiff(expected_data, json.loads(response.data), ignore_order=True) == {} + + return response + + def _test_get_report_403( + self, + user: str, + *, + job_id: Optional[int] = None, + task_id: Optional[int] = None, + project_id: Optional[int] = None, + **kwargs, + ): + params = self._get_query_params(job_id=job_id, task_id=task_id, project_id=project_id) + with make_api_client(user) as api_client: + (_, response) = api_client.analytics_api.get_reports( + **params, **kwargs, _parse_response=False, _check_status=False + ) + assert response.status == HTTPStatus.FORBIDDEN + + return response + + @pytest.mark.usefixtures("restore_db_per_function") + @pytest.mark.parametrize("is_staff, allow", [(True, True), (False, False)]) + def test_user_get_report_in_sandbox_task( + self, tasks, users, is_task_staff, is_staff, allow, admin_user + ): + task = next( + t + for t in tasks + if t["organization"] is None and not users[t["owner"]["id"]]["is_superuser"] + ) + + if is_staff: + user = task["owner"]["username"] + else: + user = next(u for u in users if not is_task_staff(u["id"], task["id"]))["username"] + + self.create_analytics_report(admin_user, task_id=task["id"]) + + if allow: + self._test_get_report_200(user, task_id=task["id"]) + else: + self._test_get_report_403(user, task_id=task["id"]) + + @pytest.mark.usefixtures("restore_db_per_function") + @pytest.mark.parametrize("is_staff, allow", [(True, True), (False, False)]) + def test_user_get_report_in_sandbox_job( + self, jobs, users, is_job_staff, is_staff, allow, admin_user + ): + job = next(j for j in jobs if j["assignee"] is not None and j["type"] != "ground_truth") + + if is_staff: + user = job["assignee"]["username"] + else: + user = next(u for u in users if not is_job_staff(u["id"], job["id"]))["username"] + + self.create_analytics_report(admin_user, job_id=job["id"]) + + if allow: + self._test_get_report_200(user, job_id=job["id"]) + else: + self._test_get_report_403(user, job_id=job["id"]) + + @pytest.mark.usefixtures("restore_db_per_function") + @pytest.mark.parametrize("is_staff, allow", [(True, True), (False, False)]) + def test_user_get_report_in_sandbox_project( + self, projects, users, is_project_staff, is_staff, allow, admin_user + ): + project = next( + p + for p in projects + if p["organization"] is None and not users[p["owner"]["id"]]["is_superuser"] + ) + + if is_staff: + user = project["owner"]["username"] + else: + user = next(u for u in users if not is_project_staff(u["id"], project["id"]))[ + "username" + ] + + self.create_analytics_report(admin_user, project_id=project["id"]) + + if allow: + self._test_get_report_200(user, project_id=project["id"]) + else: + self._test_get_report_403(user, project_id=project["id"]) + + @pytest.mark.usefixtures("restore_db_per_function") + @pytest.mark.parametrize( + "org_role, is_staff, allow", + [ + ("owner", True, True), + ("owner", False, True), + ("maintainer", True, True), + ("maintainer", False, True), + ("supervisor", True, True), + ("supervisor", False, False), + ("worker", True, True), + ("worker", False, False), + ], + ) + def test_user_get_report_in_org_task( + self, + tasks, + users, + is_org_member, + is_task_staff, + org_role, + is_staff, + allow, + admin_user, + ): + for user in users: + if user["is_superuser"]: + continue + + task = next( + ( + t + for t in tasks + if t["organization"] is not None + and is_task_staff(user["id"], t["id"]) == is_staff + and is_org_member(user["id"], t["organization"], role=org_role) + ), + None, + ) + if task is not None: + break + + assert task + + self.create_analytics_report(admin_user, task_id=task["id"]) + + if allow: + self._test_get_report_200(user["username"], task_id=task["id"]) + else: + self._test_get_report_403(user["username"], task_id=task["id"]) + + @pytest.mark.usefixtures("restore_db_per_function") + @pytest.mark.parametrize( + "org_role, is_staff, allow", + [ + ("owner", True, True), + ("owner", False, True), + ("maintainer", True, True), + ("maintainer", False, True), + ("supervisor", True, True), + ("supervisor", False, False), + ("worker", True, True), + ("worker", False, False), + ], + ) + def test_user_get_report_in_org_job( + self, + jobs, + users, + is_org_member, + is_job_staff, + org_role, + is_staff, + allow, + admin_user, + ): + for user in users: + if user["is_superuser"]: + continue + + job = next( + ( + j + for j in jobs + if j["organization"] is not None + and is_job_staff(user["id"], j["id"]) == is_staff + and is_org_member(user["id"], j["organization"], role=org_role) + ), + None, + ) + if job is not None: + break + + assert job + + self.create_analytics_report(admin_user, job_id=job["id"]) + + if allow: + self._test_get_report_200(user["username"], job_id=job["id"]) + else: + self._test_get_report_403(user["username"], job_id=job["id"]) + + @pytest.mark.usefixtures("restore_db_per_function") + @pytest.mark.parametrize( + "org_role, is_staff, allow", + [ + ("owner", True, True), + ("owner", False, True), + ("maintainer", False, True), + ("supervisor", True, True), + ("supervisor", False, False), + ("worker", True, True), + ("worker", False, False), + ], + ) + def test_user_get_report_in_org_project( + self, + projects, + users, + is_org_member, + is_project_staff, + org_role, + is_staff, + allow, + admin_user, + ): + for user in users: + if user["is_superuser"]: + continue + + project = next( + ( + p + for p in projects + if p["organization"] is not None + and is_project_staff(user["id"], p["id"]) == is_staff + and is_org_member(user["id"], p["organization"], role=org_role) + ), + None, + ) + if project is not None: + break + + assert project + + self.create_analytics_report(admin_user, project_id=project["id"]) + + if allow: + self._test_get_report_200(user["username"], project_id=project["id"]) + else: + self._test_get_report_403(user["username"], project_id=project["id"]) + + @pytest.mark.usefixtures("restore_db_per_function") + @pytest.mark.parametrize("is_staff, allow", [(True, True), (False, False)]) + def test_user_get_empty_report_in_sandbox_task( + self, tasks, users, is_task_staff, is_staff, allow, admin_user + ): + task = next( + t + for t in tasks + if t["organization"] is None and not users[t["owner"]["id"]]["is_superuser"] + ) + + if is_staff: + user = task["owner"]["username"] + else: + user = next(u for u in users if not is_task_staff(u["id"], task["id"]))["username"] + + if allow: + self._test_get_report_200(user, task_id=task["id"]) + else: + self._test_get_report_403(user, task_id=task["id"]) + + @pytest.mark.usefixtures("restore_db_per_function") + @pytest.mark.parametrize("is_staff, allow", [(True, True), (False, False)]) + def test_user_get_empty_report_in_sandbox_job( + self, jobs, users, is_job_staff, is_staff, allow, admin_user + ): + job = next(j for j in jobs if j["assignee"] is not None and j["type"] != "ground_truth") + + if is_staff: + user = job["assignee"]["username"] + else: + user = next(u for u in users if not is_job_staff(u["id"], job["id"]))["username"] + + if allow: + self._test_get_report_200(user, job_id=job["id"]) + else: + self._test_get_report_403(user, job_id=job["id"]) + + @pytest.mark.usefixtures("restore_db_per_function") + @pytest.mark.parametrize("is_staff, allow", [(True, True), (False, False)]) + def test_user_get_empty_report_in_sandbox_project( + self, projects, users, is_project_staff, is_staff, allow, admin_user + ): + project = next( + p + for p in projects + if p["organization"] is None and not users[p["owner"]["id"]]["is_superuser"] + ) + + if is_staff: + user = project["owner"]["username"] + else: + user = next(u for u in users if not is_project_staff(u["id"], project["id"]))[ + "username" + ] + + if allow: + self._test_get_report_200(user, project_id=project["id"]) + else: + self._test_get_report_403(user, project_id=project["id"]) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 14ce2f5fbf6f..98c443b290bd 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -517,6 +517,22 @@ def test_member_update_task_annotation( self._test_check_response(is_allow, response, data) + def test_remove_first_keyframe(self): + endpoint = "tasks/8/annotations" + shapes0 = [ + {"type": "rectangle", "frame": 1, "points": [1, 2, 3, 4]}, + {"type": "rectangle", "frame": 4, "points": [5, 6, 7, 8]}, + ] + + annotations = {"tracks": [{"label_id": 13, "frame": 0, "shapes": shapes0}]} + + response = patch_method("admin1", endpoint, annotations, action="create") + assert response.status_code == HTTPStatus.OK, response.content + + annotations["tracks"][0]["shapes"] = shapes0[1:] + response = patch_method("admin1", endpoint, annotations, action="update") + assert response.status_code == HTTPStatus.OK + @pytest.mark.usefixtures("restore_db_per_class") class TestGetTaskDataset: @@ -635,6 +651,27 @@ def test_can_export_task_to_coco_format(self, admin_user, tid): assert annotations["tracks"][0]["shapes"][0]["frame"] == 0 assert annotations["tracks"][0]["elements"][0]["shapes"][0]["frame"] == 0 + @pytest.mark.usefixtures("restore_db_per_function") + def test_can_download_task_with_special_chars_in_name(self, admin_user): + # Control characters in filenames may conflict with the Content-Disposition header + # value restrictions, as it needs to include the downloaded file name. + + task_spec = { + "name": "test_special_chars_{}_in_name".format("".join(chr(c) for c in range(1, 127))), + "labels": [{"name": "cat"}], + } + + task_data = { + "image_quality": 75, + "client_files": generate_image_files(1), + } + + task_id, _ = create_task(admin_user, task_spec, task_data) + + response = self._test_export_task(admin_user, task_id, format="CVAT for images 1.1") + assert response.status == HTTPStatus.OK + assert zipfile.is_zipfile(io.BytesIO(response.data)) + @pytest.mark.usefixtures("restore_db_per_function") @pytest.mark.usefixtures("restore_cvat_data") @@ -2279,6 +2316,7 @@ def _init_tasks(cls): cls.data[key] = (task, dataset_file) + @pytest.mark.skip("Fails sometimes, needs to be fixed") @pytest.mark.parametrize( "task_kind, annotation_kind, expect_success", [ diff --git a/tests/python/sdk/test_api_wrappers.py b/tests/python/sdk/test_api_wrappers.py new file mode 100644 index 000000000000..84ec919c9ba2 --- /dev/null +++ b/tests/python/sdk/test_api_wrappers.py @@ -0,0 +1,114 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from copy import deepcopy + +from cvat_sdk import models +from deepdiff import DeepDiff + + +def test_models_do_not_change_input_values(): + # Nested containers may be modified during the model input data parsing. + # This can lead to subtle memory errors, which are very hard to find. + original_input_data = { + "name": "test", + "labels": [ + { + "name": "cat", + "attributes": [ + { + "default_value": "yy", + "input_type": "text", + "mutable": False, + "name": "x", + "values": ["yy"], + }, + { + "default_value": "1", + "input_type": "radio", + "mutable": False, + "name": "y", + "values": ["1", "2"], + }, + ], + } + ], + } + + input_data = deepcopy(original_input_data) + + models.TaskWriteRequest(**input_data) + + assert DeepDiff(original_input_data, input_data) == {} + + +def test_models_do_not_store_input_collections(): + # Avoid depending on input data for collection fields after the model is initialized. + # This can lead to subtle memory errors and unexpected behavior + # if the original input data is modified. + input_data = { + "name": "test", + "labels": [ + { + "name": "cat1", + "attributes": [ + { + "default_value": "yy", + "input_type": "text", + "mutable": False, + "name": "x", + "values": ["yy"], + }, + { + "default_value": "1", + "input_type": "radio", + "mutable": False, + "name": "y", + "values": ["1", "2"], + }, + ], + }, + {"name": "cat2", "attributes": []}, + ], + } + + model = models.TaskWriteRequest(**input_data) + model_data1 = model.to_dict() + + # Modify input value containers + input_data["labels"][0]["attributes"].clear() + input_data["labels"][1]["attributes"].append( + { + "default_value": "", + "input_type": "text", + "mutable": True, + "name": "z", + } + ) + input_data["labels"].append({"name": "dog"}) + + model_data2 = model.to_dict() + + assert DeepDiff(model_data1, model_data2) == {} + + +def test_models_do_not_return_internal_collections(): + # Avoid returning internal data for mutable collection fields. + # This can lead to subtle memory errors and unexpected behavior + # if the returned data is modified. + input_data = { + "name": "test", + "labels": [], + } + + model = models.TaskWriteRequest(**input_data) + model_data1 = model.to_dict() + model_data1_original = deepcopy(model_data1) + + # Modify an output value container + model_data1["labels"].append({"name": "dog"}) + + model_data2 = model.to_dict() + + assert DeepDiff(model_data1_original, model_data2) == {} diff --git a/tests/python/sdk/test_datasets.py b/tests/python/sdk/test_datasets.py new file mode 100644 index 000000000000..67204e4c26c9 --- /dev/null +++ b/tests/python/sdk/test_datasets.py @@ -0,0 +1,207 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +import io +from logging import Logger +from pathlib import Path +from typing import Tuple + +import cvat_sdk.datasets as cvatds +import PIL.Image +import pytest +from cvat_sdk import Client, models +from cvat_sdk.core.proxies.tasks import ResourceType + +from shared.utils.helpers import generate_image_files + +from .util import restrict_api_requests + + +@pytest.fixture(autouse=True) +def _common_setup( + tmp_path: Path, + fxt_login: Tuple[Client, str], + fxt_logger: Tuple[Logger, io.StringIO], +): + logger = fxt_logger[0] + client = fxt_login[0] + client.logger = logger + client.config.cache_dir = tmp_path / "cache" + + api_client = client.api_client + for k in api_client.configuration.logger: + api_client.configuration.logger[k] = logger + + +class TestTaskDataset: + @pytest.fixture(autouse=True) + def setup( + self, + tmp_path: Path, + fxt_login: Tuple[Client, str], + ): + self.client = fxt_login[0] + self.images = generate_image_files(10) + + image_dir = tmp_path / "images" + image_dir.mkdir() + + image_paths = [] + for image in self.images: + image_path = image_dir / image.name + image_path.write_bytes(image.getbuffer()) + image_paths.append(image_path) + + self.task = self.client.tasks.create_from_data( + models.TaskWriteRequest( + "Dataset layer test task", + labels=[ + models.PatchedLabelRequest(name="person"), + models.PatchedLabelRequest(name="car"), + ], + ), + resource_type=ResourceType.LOCAL, + resources=image_paths, + data_params={"chunk_size": 3}, + ) + + self.expected_labels = sorted(self.task.get_labels(), key=lambda l: l.id) + + self.task.update_annotations( + models.PatchedLabeledDataRequest( + tags=[ + models.LabeledImageRequest(frame=8, label_id=self.expected_labels[0].id), + models.LabeledImageRequest(frame=8, label_id=self.expected_labels[1].id), + ], + shapes=[ + models.LabeledShapeRequest( + frame=6, + label_id=self.expected_labels[1].id, + type=models.ShapeType("rectangle"), + points=[1.0, 2.0, 3.0, 4.0], + ), + ], + ) + ) + + def test_basic(self): + dataset = cvatds.TaskDataset(self.client, self.task.id) + + # verify that the cache is not empty + assert list(self.client.config.cache_dir.iterdir()) + + for expected_label, actual_label in zip( + self.expected_labels, sorted(dataset.labels, key=lambda l: l.id) + ): + assert expected_label.id == actual_label.id + assert expected_label.name == actual_label.name + + assert len(dataset.samples) == self.task.size + + for index, sample in enumerate(dataset.samples): + assert sample.frame_index == index + + actual_image = sample.media.load_image() + expected_image = PIL.Image.open(self.images[index]) + + assert actual_image == expected_image + + assert not dataset.samples[0].annotations.tags + assert not dataset.samples[1].annotations.shapes + + assert {tag.label_id for tag in dataset.samples[8].annotations.tags} == { + label.id for label in self.expected_labels + } + assert not dataset.samples[8].annotations.shapes + + assert not dataset.samples[6].annotations.tags + assert len(dataset.samples[6].annotations.shapes) == 1 + assert dataset.samples[6].annotations.shapes[0].type.value == "rectangle" + assert dataset.samples[6].annotations.shapes[0].points == [1.0, 2.0, 3.0, 4.0] + + def test_deleted_frame(self): + self.task.remove_frames_by_ids([1]) + + dataset = cvatds.TaskDataset(self.client, self.task.id) + + assert len(dataset.samples) == self.task.size - 1 + + # sample #0 is still frame #0 + assert dataset.samples[0].frame_index == 0 + assert dataset.samples[0].media.load_image() == PIL.Image.open(self.images[0]) + + # sample #1 is now frame #2 + assert dataset.samples[1].frame_index == 2 + assert dataset.samples[1].media.load_image() == PIL.Image.open(self.images[2]) + + # sample #5 is now frame #6 + assert dataset.samples[5].frame_index == 6 + assert dataset.samples[5].media.load_image() == PIL.Image.open(self.images[6]) + assert len(dataset.samples[5].annotations.shapes) == 1 + + def test_offline(self, monkeypatch: pytest.MonkeyPatch): + dataset = cvatds.TaskDataset( + self.client, + self.task.id, + update_policy=cvatds.UpdatePolicy.IF_MISSING_OR_STALE, + ) + + fresh_samples = list(dataset.samples) + + restrict_api_requests(monkeypatch) + + dataset = cvatds.TaskDataset( + self.client, + self.task.id, + update_policy=cvatds.UpdatePolicy.NEVER, + ) + + cached_samples = list(dataset.samples) + + for fresh_sample, cached_sample in zip(fresh_samples, cached_samples): + assert fresh_sample.frame_index == cached_sample.frame_index + assert fresh_sample.annotations == cached_sample.annotations + assert fresh_sample.media.load_image() == cached_sample.media.load_image() + + def test_update(self, monkeypatch: pytest.MonkeyPatch): + dataset = cvatds.TaskDataset( + self.client, + self.task.id, + ) + + # Recreating the dataset should only result in minimal requests. + restrict_api_requests( + monkeypatch, allow_paths={f"/api/tasks/{self.task.id}", "/api/labels"} + ) + + dataset = cvatds.TaskDataset( + self.client, + self.task.id, + ) + + assert dataset.samples[6].annotations.shapes[0].label_id == self.expected_labels[1].id + + # After an update, the annotations should be redownloaded. + monkeypatch.undo() + + self.task.update_annotations( + models.PatchedLabeledDataRequest( + shapes=[ + models.LabeledShapeRequest( + id=dataset.samples[6].annotations.shapes[0].id, + frame=6, + label_id=self.expected_labels[0].id, + type=models.ShapeType("rectangle"), + points=[1.0, 2.0, 3.0, 4.0], + ), + ] + ) + ) + + dataset = cvatds.TaskDataset( + self.client, + self.task.id, + ) + + assert dataset.samples[6].annotations.shapes[0].label_id == self.expected_labels[0].id diff --git a/tests/python/sdk/test_jobs.py b/tests/python/sdk/test_jobs.py index 59a811c7fa8f..4604ab060fcc 100644 --- a/tests/python/sdk/test_jobs.py +++ b/tests/python/sdk/test_jobs.py @@ -149,7 +149,12 @@ def test_can_download_frames(self, fxt_new_task: Task, quality: str): filename_pattern="frame-{frame_id}{frame_ext}", ) - assert (self.tmp_path / "frame-0.jpg").is_file() + if quality == "original": + expected_frame_ext = "png" + else: + expected_frame_ext = "jpg" + + assert (self.tmp_path / f"frame-0.{expected_frame_ext}").is_file() assert self.stdout.getvalue() == "" def test_can_upload_annotations(self, fxt_new_task: Task, fxt_coco_file: Path): diff --git a/tests/python/sdk/test_projects.py b/tests/python/sdk/test_projects.py index 74ecf5e69084..852a286fc288 100644 --- a/tests/python/sdk/test_projects.py +++ b/tests/python/sdk/test_projects.py @@ -115,6 +115,30 @@ def test_can_create_empty_project(self): assert project.id != 0 assert project.name == "test project" + def test_can_create_project_with_attribute_with_blank_default(self): + project = self.client.projects.create( + spec=models.ProjectWriteRequest( + name="test project", + labels=[ + models.PatchedLabelRequest( + name="text", + attributes=[ + models.AttributeRequest( + name="text", + mutable=True, + input_type=models.InputTypeEnum("text"), + values=[], + default_value="", + ) + ], + ) + ], + ) + ) + + labels = project.get_labels() + assert labels[0].attributes[0].default_value == "" + def test_can_create_project_from_dataset(self, fxt_coco_dataset: Path): pbar_out = io.StringIO() pbar = make_pbar(file=pbar_out) diff --git a/tests/python/sdk/test_pytorch.py b/tests/python/sdk/test_pytorch.py index bcfedf7b8b3b..722cb37ab003 100644 --- a/tests/python/sdk/test_pytorch.py +++ b/tests/python/sdk/test_pytorch.py @@ -7,12 +7,10 @@ import os from logging import Logger from pathlib import Path -from typing import Container, Tuple -from urllib.parse import urlparse +from typing import Tuple import pytest from cvat_sdk import Client, models -from cvat_sdk.api_client.rest import RESTClientObject from cvat_sdk.core.proxies.tasks import ResourceType try: @@ -30,6 +28,8 @@ from shared.utils.helpers import generate_image_files +from .util import restrict_api_requests + @pytest.fixture(autouse=True) def _common_setup( @@ -47,20 +47,6 @@ def _common_setup( api_client.configuration.logger[k] = logger -def _restrict_api_requests( - monkeypatch: pytest.MonkeyPatch, allow_paths: Container[str] = () -) -> None: - original_request = RESTClientObject.request - - def restricted_request(self, method, url, *args, **kwargs): - parsed_url = urlparse(url) - if parsed_url.path in allow_paths: - return original_request(self, method, url, *args, **kwargs) - raise RuntimeError("Disallowed!") - - monkeypatch.setattr(RESTClientObject, "request", restricted_request) - - @pytest.mark.skipif(cvatpt is None, reason="PyTorch dependencies are not installed") class TestTaskVisionDataset: @pytest.fixture(autouse=True) @@ -254,7 +240,7 @@ def test_offline(self, monkeypatch: pytest.MonkeyPatch): fresh_samples = list(dataset) - _restrict_api_requests(monkeypatch) + restrict_api_requests(monkeypatch) dataset = cvatpt.TaskVisionDataset( self.client, @@ -273,7 +259,7 @@ def test_update(self, monkeypatch: pytest.MonkeyPatch): ) # Recreating the dataset should only result in minimal requests. - _restrict_api_requests( + restrict_api_requests( monkeypatch, allow_paths={f"/api/tasks/{self.task.id}", "/api/labels"} ) @@ -447,7 +433,7 @@ def test_offline(self, monkeypatch: pytest.MonkeyPatch): fresh_samples = list(dataset) - _restrict_api_requests(monkeypatch) + restrict_api_requests(monkeypatch) dataset = cvatpt.ProjectVisionDataset( self.client, diff --git a/tests/python/sdk/test_tasks.py b/tests/python/sdk/test_tasks.py index 8a5bbf8b5a58..dd5c7b8f2119 100644 --- a/tests/python/sdk/test_tasks.py +++ b/tests/python/sdk/test_tasks.py @@ -365,7 +365,12 @@ def test_can_download_frames(self, fxt_new_task: Task, quality: str): filename_pattern="frame-{frame_id}{frame_ext}", ) - assert (self.tmp_path / "frame-0.jpg").is_file() + if quality == "original": + expected_frame_ext = "png" + else: + expected_frame_ext = "jpg" + + assert (self.tmp_path / f"frame-0.{expected_frame_ext}").is_file() assert self.stdout.getvalue() == "" @pytest.mark.parametrize("quality", ("compressed", "original")) diff --git a/tests/python/sdk/util.py b/tests/python/sdk/util.py index 83e6b10e2908..5861c658111a 100644 --- a/tests/python/sdk/util.py +++ b/tests/python/sdk/util.py @@ -4,8 +4,11 @@ import textwrap from pathlib import Path -from typing import Tuple +from typing import Container, Tuple +from urllib.parse import urlparse +import pytest +from cvat_sdk.api_client.rest import RESTClientObject from cvat_sdk.core.helpers import TqdmProgressReporter from tqdm import tqdm @@ -82,3 +85,17 @@ def generate_coco_anno(image_path: str, image_width: int, image_height: int) -> "image_width": image_width, } ) + + +def restrict_api_requests( + monkeypatch: pytest.MonkeyPatch, allow_paths: Container[str] = () +) -> None: + original_request = RESTClientObject.request + + def restricted_request(self, method, url, *args, **kwargs): + parsed_url = urlparse(url) + if parsed_url.path in allow_paths: + return original_request(self, method, url, *args, **kwargs) + raise RuntimeError("Disallowed!") + + monkeypatch.setattr(RESTClientObject, "request", restricted_request) diff --git a/tests/python/shared/assets/cvat_db/data.json b/tests/python/shared/assets/cvat_db/data.json index b33aad592e9c..e9653d178cc8 100644 --- a/tests/python/shared/assets/cvat_db/data.json +++ b/tests/python/shared/assets/cvat_db/data.json @@ -10255,7 +10255,7 @@ "created_date": "2023-05-26T16:25:36.613Z", "target_last_updated": "2023-05-26T16:17:02.635Z", "gt_last_updated": "2023-05-26T16:16:50.630Z", - "data": "{\"parameters\":{\"included_annotation_types\":[\"bbox\",\"points\",\"mask\",\"polygon\",\"polyline\",\"skeleton\"],\"compare_attributes\":true,\"ignored_attributes\":[],\"iou_threshold\":0.4,\"low_overlap_threshold\":0.8,\"oks_sigma\":0.09,\"line_thickness\":0.01,\"compare_line_orientation\":true,\"line_orientation_threshold\":0.1,\"compare_groups\":true,\"group_match_threshold\":0.5,\"check_covered_annotations\":true,\"object_visibility_threshold\":0.05,\"panoptic_comparison\":true},\"comparison_summary\":{\"frame_share\":0.2727272727272727,\"frames\":[0,1,2],\"conflict_count\":37,\"warning_count\":15,\"error_count\":22,\"conflicts_by_type\":{\"low_overlap\":6,\"missing_annotation\":9,\"extra_annotation\":10,\"mismatching_label\":3,\"mismatching_direction\":1,\"covered_annotation\":1,\"mismatching_attributes\":1,\"mismatching_groups\":6},\"annotations\":{\"valid_count\":21,\"missing_count\":9,\"extra_count\":10,\"total_count\":43,\"ds_count\":34,\"gt_count\":33,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,2,3],[1,21,7],[1,8,0]],\"precision\":[0.0,0.7241379310344828,0.0],\"recall\":[0.0,0.6774193548387096,0.0],\"accuracy\":[0.0,0.5384615384615384,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.4883720930232558,\"precision\":0.6176470588235294,\"recall\":0.6363636363636364},\"annotation_components\":{\"shape\":{\"valid_count\":24,\"missing_count\":9,\"extra_count\":10,\"total_count\":43,\"ds_count\":34,\"gt_count\":33,\"mean_iou\":0.1773115335759765,\"accuracy\":0.5581395348837209},\"label\":{\"valid_count\":21,\"invalid_count\":3,\"total_count\":24,\"accuracy\":0.875}},\"frame_count\":3,\"mean_conflict_count\":12.333333333333334},\"frame_results\":{\"0\":{\"conflicts\":[{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":65,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":101,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":91,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":118,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":88,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":102,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":68,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"},{\"obj_id\":98,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":67,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"},{\"obj_id\":99,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":92,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":119,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":107,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":121,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":122,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":70,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":76,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":93,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":74,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":95,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":73,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":96,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"ellipse\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":64,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":66,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":94,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_label\",\"annotation_ids\":[{\"obj_id\":89,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":123,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_label\",\"annotation_ids\":[{\"obj_id\":92,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":119,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_label\",\"annotation_ids\":[{\"obj_id\":69,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"mask\"},{\"obj_id\":97,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_direction\",\"annotation_ids\":[{\"obj_id\":67,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"},{\"obj_id\":99,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"covered_annotation\",\"annotation_ids\":[{\"obj_id\":69,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"mask\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_attributes\",\"annotation_ids\":[{\"obj_id\":65,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":101,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":82,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":103,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":81,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":104,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":83,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":105,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":87,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":106,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":80,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":111,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":77,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":114,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"}],\"annotations\":{\"valid_count\":21,\"missing_count\":3,\"extra_count\":10,\"total_count\":37,\"ds_count\":34,\"gt_count\":27,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,2,3],[1,21,7],[1,2,0]],\"precision\":[0.0,0.7241379310344828,0.0],\"recall\":[0.0,0.84,0.0],\"accuracy\":[0.0,0.6363636363636364,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.5675675675675675,\"precision\":0.6176470588235294,\"recall\":0.7777777777777778},\"annotation_components\":{\"shape\":{\"valid_count\":24,\"missing_count\":3,\"extra_count\":10,\"total_count\":37,\"ds_count\":34,\"gt_count\":27,\"mean_iou\":0.5319346007279295,\"accuracy\":0.6486486486486487},\"label\":{\"valid_count\":21,\"invalid_count\":3,\"total_count\":24,\"accuracy\":0.875}},\"conflict_count\":31,\"warning_count\":15,\"error_count\":16,\"conflicts_by_type\":{\"low_overlap\":6,\"missing_annotation\":3,\"extra_annotation\":10,\"mismatching_label\":3,\"mismatching_direction\":1,\"covered_annotation\":1,\"mismatching_attributes\":1,\"mismatching_groups\":6}},\"1\":{\"conflicts\":[{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":130,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":128,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":124,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":125,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"ellipse\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":127,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"mask\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":129,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"}],\"annotations\":{\"valid_count\":0,\"missing_count\":6,\"extra_count\":0,\"total_count\":6,\"ds_count\":0,\"gt_count\":6,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,0,0],[0,0,0],[0,6,0]],\"precision\":[0.0,0.0,0.0],\"recall\":[0.0,0.0,0.0],\"accuracy\":[0.0,0.0,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.0,\"precision\":0.0,\"recall\":0.0},\"annotation_components\":{\"shape\":{\"valid_count\":0,\"missing_count\":6,\"extra_count\":0,\"total_count\":6,\"ds_count\":0,\"gt_count\":6,\"mean_iou\":0.0,\"accuracy\":0.0},\"label\":{\"valid_count\":0,\"invalid_count\":0,\"total_count\":0,\"accuracy\":0.0}},\"conflict_count\":6,\"warning_count\":0,\"error_count\":6,\"conflicts_by_type\":{\"missing_annotation\":6}},\"2\":{\"conflicts\":[],\"annotations\":{\"valid_count\":0,\"missing_count\":0,\"extra_count\":0,\"total_count\":0,\"ds_count\":0,\"gt_count\":0,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,0,0],[0,0,0],[0,0,0]],\"precision\":[0.0,0.0,0.0],\"recall\":[0.0,0.0,0.0],\"accuracy\":[0.0,0.0,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.0,\"precision\":0.0,\"recall\":0.0},\"annotation_components\":{\"shape\":{\"valid_count\":0,\"missing_count\":0,\"extra_count\":0,\"total_count\":0,\"ds_count\":0,\"gt_count\":0,\"mean_iou\":0,\"accuracy\":0.0},\"label\":{\"valid_count\":0,\"invalid_count\":0,\"total_count\":0,\"accuracy\":0.0}},\"conflict_count\":0,\"warning_count\":0,\"error_count\":0,\"conflicts_by_type\":{}}}}" + "data": "{\"parameters\":{\"included_annotation_types\":[\"bbox\",\"points\",\"mask\",\"polygon\",\"polyline\",\"skeleton\"],\"compare_attributes\":true,\"ignored_attributes\":[],\"iou_threshold\":0.4,\"low_overlap_threshold\":0.8,\"oks_sigma\":0.09,\"line_thickness\":0.01,\"compare_line_orientation\":true,\"line_orientation_threshold\":0.1,\"compare_groups\":true,\"group_match_threshold\":0.5,\"check_covered_annotations\":true,\"object_visibility_threshold\":0.05,\"panoptic_comparison\":true},\"comparison_summary\":{\"frame_share\":0.2727272727272727,\"frames\":[0,1,2],\"conflict_count\":37,\"warning_count\":15,\"error_count\":22,\"conflicts_by_type\":{\"low_overlap\":6,\"missing_annotation\":9,\"extra_annotation\":10,\"mismatching_label\":3,\"mismatching_direction\":1,\"covered_annotation\":1,\"mismatching_attributes\":1,\"mismatching_groups\":6},\"annotations\":{\"valid_count\":21,\"missing_count\":9,\"extra_count\":10,\"total_count\":43,\"ds_count\":34,\"gt_count\":33,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,2,3],[1,21,7],[1,8,0]],\"precision\":[0.0,0.7241379310344828,0.0],\"recall\":[0.0,0.6774193548387096,0.0],\"accuracy\":[0.0,0.5384615384615384,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.4883720930232558,\"precision\":0.6176470588235294,\"recall\":0.6363636363636364},\"annotation_components\":{\"shape\":{\"valid_count\":24,\"missing_count\":9,\"extra_count\":10,\"total_count\":43,\"ds_count\":34,\"gt_count\":33,\"mean_iou\":0.19532955159399454,\"accuracy\":0.5581395348837209},\"label\":{\"valid_count\":21,\"invalid_count\":3,\"total_count\":24,\"accuracy\":0.875}},\"frame_count\":3,\"mean_conflict_count\":12.333333333333334},\"frame_results\":{\"0\":{\"conflicts\":[{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":65,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":101,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":91,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":118,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":88,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":102,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":68,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"},{\"obj_id\":98,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":67,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"},{\"obj_id\":99,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":92,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":119,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":107,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":121,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":122,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":70,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":76,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":93,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":74,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":95,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":73,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":96,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"ellipse\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":64,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":66,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":94,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_label\",\"annotation_ids\":[{\"obj_id\":89,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":123,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_label\",\"annotation_ids\":[{\"obj_id\":92,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":119,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_label\",\"annotation_ids\":[{\"obj_id\":69,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"mask\"},{\"obj_id\":97,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_direction\",\"annotation_ids\":[{\"obj_id\":67,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"},{\"obj_id\":99,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"covered_annotation\",\"annotation_ids\":[{\"obj_id\":69,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"mask\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_attributes\",\"annotation_ids\":[{\"obj_id\":65,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":101,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":82,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":103,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":81,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":104,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":83,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":105,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":87,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":106,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":80,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":111,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":77,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":114,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"}],\"annotations\":{\"valid_count\":21,\"missing_count\":3,\"extra_count\":10,\"total_count\":37,\"ds_count\":34,\"gt_count\":27,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,2,3],[1,21,7],[1,2,0]],\"precision\":[0.0,0.7241379310344828,0.0],\"recall\":[0.0,0.84,0.0],\"accuracy\":[0.0,0.6363636363636364,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.5675675675675675,\"precision\":0.6176470588235294,\"recall\":0.7777777777777778},\"annotation_components\":{\"shape\":{\"valid_count\":24,\"missing_count\":3,\"extra_count\":10,\"total_count\":37,\"ds_count\":34,\"gt_count\":27,\"mean_iou\":0.5859886547819836,\"accuracy\":0.6486486486486487},\"label\":{\"valid_count\":21,\"invalid_count\":3,\"total_count\":24,\"accuracy\":0.875}},\"conflict_count\":31,\"warning_count\":15,\"error_count\":16,\"conflicts_by_type\":{\"low_overlap\":6,\"missing_annotation\":3,\"extra_annotation\":10,\"mismatching_label\":3,\"mismatching_direction\":1,\"covered_annotation\":1,\"mismatching_attributes\":1,\"mismatching_groups\":6}},\"1\":{\"conflicts\":[{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":130,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":128,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":124,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":125,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"ellipse\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":127,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"mask\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":129,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"}],\"annotations\":{\"valid_count\":0,\"missing_count\":6,\"extra_count\":0,\"total_count\":6,\"ds_count\":0,\"gt_count\":6,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,0,0],[0,0,0],[0,6,0]],\"precision\":[0.0,0.0,0.0],\"recall\":[0.0,0.0,0.0],\"accuracy\":[0.0,0.0,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.0,\"precision\":0.0,\"recall\":0.0},\"annotation_components\":{\"shape\":{\"valid_count\":0,\"missing_count\":6,\"extra_count\":0,\"total_count\":6,\"ds_count\":0,\"gt_count\":6,\"mean_iou\":0.0,\"accuracy\":0.0},\"label\":{\"valid_count\":0,\"invalid_count\":0,\"total_count\":0,\"accuracy\":0.0}},\"conflict_count\":6,\"warning_count\":0,\"error_count\":6,\"conflicts_by_type\":{\"missing_annotation\":6}},\"2\":{\"conflicts\":[],\"annotations\":{\"valid_count\":0,\"missing_count\":0,\"extra_count\":0,\"total_count\":0,\"ds_count\":0,\"gt_count\":0,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,0,0],[0,0,0],[0,0,0]],\"precision\":[0.0,0.0,0.0],\"recall\":[0.0,0.0,0.0],\"accuracy\":[0.0,0.0,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.0,\"precision\":0.0,\"recall\":0.0},\"annotation_components\":{\"shape\":{\"valid_count\":0,\"missing_count\":0,\"extra_count\":0,\"total_count\":0,\"ds_count\":0,\"gt_count\":0,\"mean_iou\":0,\"accuracy\":0.0},\"label\":{\"valid_count\":0,\"invalid_count\":0,\"total_count\":0,\"accuracy\":0.0}},\"conflict_count\":0,\"warning_count\":0,\"error_count\":0,\"conflicts_by_type\":{}}}}" } }, { @@ -10268,7 +10268,7 @@ "created_date": "2023-05-26T16:25:36.616Z", "target_last_updated": "2023-05-26T16:11:24.294Z", "gt_last_updated": "2023-05-26T16:16:50.630Z", - "data": "{\"parameters\":{\"included_annotation_types\":[\"bbox\",\"points\",\"mask\",\"polygon\",\"polyline\",\"skeleton\"],\"compare_attributes\":true,\"ignored_attributes\":[],\"iou_threshold\":0.4,\"low_overlap_threshold\":0.8,\"oks_sigma\":0.09,\"line_thickness\":0.01,\"compare_line_orientation\":true,\"line_orientation_threshold\":0.1,\"compare_groups\":true,\"group_match_threshold\":0.5,\"check_covered_annotations\":true,\"object_visibility_threshold\":0.05,\"panoptic_comparison\":true},\"comparison_summary\":{\"frame_share\":0.2727272727272727,\"frames\":[0,1,2],\"conflict_count\":37,\"warning_count\":15,\"error_count\":22,\"conflicts_by_type\":{\"low_overlap\":6,\"missing_annotation\":9,\"extra_annotation\":10,\"mismatching_label\":3,\"mismatching_direction\":1,\"covered_annotation\":1,\"mismatching_attributes\":1,\"mismatching_groups\":6},\"annotations\":{\"valid_count\":21,\"missing_count\":9,\"extra_count\":10,\"total_count\":43,\"ds_count\":34,\"gt_count\":33,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,2,3],[1,21,7],[1,8,0]],\"precision\":[0.0,0.7241379310344828,0.0],\"recall\":[0.0,0.6774193548387096,0.0],\"accuracy\":[0.0,0.5384615384615384,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.4883720930232558,\"precision\":0.6176470588235294,\"recall\":0.6363636363636364},\"annotation_components\":{\"shape\":{\"valid_count\":24,\"missing_count\":9,\"extra_count\":10,\"total_count\":43,\"ds_count\":34,\"gt_count\":33,\"mean_iou\":0.1773115335759765,\"accuracy\":0.5581395348837209},\"label\":{\"valid_count\":21,\"invalid_count\":3,\"total_count\":24,\"accuracy\":0.875}},\"frame_count\":3,\"mean_conflict_count\":12.333333333333334},\"frame_results\":{\"0\":{\"conflicts\":[{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":65,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":101,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":91,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":118,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":88,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":102,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":68,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"},{\"obj_id\":98,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":67,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"},{\"obj_id\":99,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":92,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":119,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":107,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":121,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":122,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":70,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":76,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":93,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":74,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":95,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":73,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":96,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"ellipse\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":64,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":66,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":94,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_label\",\"annotation_ids\":[{\"obj_id\":89,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":123,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_label\",\"annotation_ids\":[{\"obj_id\":92,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":119,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_label\",\"annotation_ids\":[{\"obj_id\":69,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"mask\"},{\"obj_id\":97,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_direction\",\"annotation_ids\":[{\"obj_id\":67,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"},{\"obj_id\":99,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"covered_annotation\",\"annotation_ids\":[{\"obj_id\":69,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"mask\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_attributes\",\"annotation_ids\":[{\"obj_id\":65,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":101,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":82,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":103,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":81,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":104,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":83,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":105,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":87,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":106,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":80,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":111,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":77,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":114,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"}],\"annotations\":{\"valid_count\":21,\"missing_count\":3,\"extra_count\":10,\"total_count\":37,\"ds_count\":34,\"gt_count\":27,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,2,3],[1,21,7],[1,2,0]],\"precision\":[0.0,0.7241379310344828,0.0],\"recall\":[0.0,0.84,0.0],\"accuracy\":[0.0,0.6363636363636364,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.5675675675675675,\"precision\":0.6176470588235294,\"recall\":0.7777777777777778},\"annotation_components\":{\"shape\":{\"valid_count\":24,\"missing_count\":3,\"extra_count\":10,\"total_count\":37,\"ds_count\":34,\"gt_count\":27,\"mean_iou\":0.5319346007279295,\"accuracy\":0.6486486486486487},\"label\":{\"valid_count\":21,\"invalid_count\":3,\"total_count\":24,\"accuracy\":0.875}},\"conflict_count\":31,\"warning_count\":15,\"error_count\":16,\"conflicts_by_type\":{\"low_overlap\":6,\"missing_annotation\":3,\"extra_annotation\":10,\"mismatching_label\":3,\"mismatching_direction\":1,\"covered_annotation\":1,\"mismatching_attributes\":1,\"mismatching_groups\":6}},\"1\":{\"conflicts\":[{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":130,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":128,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":124,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":125,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"ellipse\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":127,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"mask\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":129,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"}],\"annotations\":{\"valid_count\":0,\"missing_count\":6,\"extra_count\":0,\"total_count\":6,\"ds_count\":0,\"gt_count\":6,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,0,0],[0,0,0],[0,6,0]],\"precision\":[0.0,0.0,0.0],\"recall\":[0.0,0.0,0.0],\"accuracy\":[0.0,0.0,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.0,\"precision\":0.0,\"recall\":0.0},\"annotation_components\":{\"shape\":{\"valid_count\":0,\"missing_count\":6,\"extra_count\":0,\"total_count\":6,\"ds_count\":0,\"gt_count\":6,\"mean_iou\":0.0,\"accuracy\":0.0},\"label\":{\"valid_count\":0,\"invalid_count\":0,\"total_count\":0,\"accuracy\":0.0}},\"conflict_count\":6,\"warning_count\":0,\"error_count\":6,\"conflicts_by_type\":{\"missing_annotation\":6}},\"2\":{\"conflicts\":[],\"annotations\":{\"valid_count\":0,\"missing_count\":0,\"extra_count\":0,\"total_count\":0,\"ds_count\":0,\"gt_count\":0,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,0,0],[0,0,0],[0,0,0]],\"precision\":[0.0,0.0,0.0],\"recall\":[0.0,0.0,0.0],\"accuracy\":[0.0,0.0,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.0,\"precision\":0.0,\"recall\":0.0},\"annotation_components\":{\"shape\":{\"valid_count\":0,\"missing_count\":0,\"extra_count\":0,\"total_count\":0,\"ds_count\":0,\"gt_count\":0,\"mean_iou\":0,\"accuracy\":0.0},\"label\":{\"valid_count\":0,\"invalid_count\":0,\"total_count\":0,\"accuracy\":0.0}},\"conflict_count\":0,\"warning_count\":0,\"error_count\":0,\"conflicts_by_type\":{}}}}" + "data": "{\"parameters\":{\"included_annotation_types\":[\"bbox\",\"points\",\"mask\",\"polygon\",\"polyline\",\"skeleton\"],\"compare_attributes\":true,\"ignored_attributes\":[],\"iou_threshold\":0.4,\"low_overlap_threshold\":0.8,\"oks_sigma\":0.09,\"line_thickness\":0.01,\"compare_line_orientation\":true,\"line_orientation_threshold\":0.1,\"compare_groups\":true,\"group_match_threshold\":0.5,\"check_covered_annotations\":true,\"object_visibility_threshold\":0.05,\"panoptic_comparison\":true},\"comparison_summary\":{\"frame_share\":0.2727272727272727,\"frames\":[0,1,2],\"conflict_count\":37,\"warning_count\":15,\"error_count\":22,\"conflicts_by_type\":{\"low_overlap\":6,\"missing_annotation\":9,\"extra_annotation\":10,\"mismatching_label\":3,\"mismatching_direction\":1,\"covered_annotation\":1,\"mismatching_attributes\":1,\"mismatching_groups\":6},\"annotations\":{\"valid_count\":21,\"missing_count\":9,\"extra_count\":10,\"total_count\":43,\"ds_count\":34,\"gt_count\":33,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,2,3],[1,21,7],[1,8,0]],\"precision\":[0.0,0.7241379310344828,0.0],\"recall\":[0.0,0.6774193548387096,0.0],\"accuracy\":[0.0,0.5384615384615384,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.4883720930232558,\"precision\":0.6176470588235294,\"recall\":0.6363636363636364},\"annotation_components\":{\"shape\":{\"valid_count\":24,\"missing_count\":9,\"extra_count\":10,\"total_count\":43,\"ds_count\":34,\"gt_count\":33,\"mean_iou\":0.19532955159399454,\"accuracy\":0.5581395348837209},\"label\":{\"valid_count\":21,\"invalid_count\":3,\"total_count\":24,\"accuracy\":0.875}},\"frame_count\":3,\"mean_conflict_count\":12.333333333333334},\"frame_results\":{\"0\":{\"conflicts\":[{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":65,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":101,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":91,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":118,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":88,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":102,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":68,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"},{\"obj_id\":98,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":67,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"},{\"obj_id\":99,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"low_overlap\",\"annotation_ids\":[{\"obj_id\":92,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":119,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":107,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":121,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":122,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":70,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":76,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":93,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":74,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":95,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":73,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":96,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"ellipse\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":64,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":66,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"extra_annotation\",\"annotation_ids\":[{\"obj_id\":94,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_label\",\"annotation_ids\":[{\"obj_id\":89,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":123,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_label\",\"annotation_ids\":[{\"obj_id\":92,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"points\"},{\"obj_id\":119,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_label\",\"annotation_ids\":[{\"obj_id\":69,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"mask\"},{\"obj_id\":97,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":0,\"type\":\"mismatching_direction\",\"annotation_ids\":[{\"obj_id\":67,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"polyline\"},{\"obj_id\":99,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"covered_annotation\",\"annotation_ids\":[{\"obj_id\":69,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"mask\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_attributes\",\"annotation_ids\":[{\"obj_id\":65,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":101,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":82,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":103,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":81,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":104,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":83,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":105,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":87,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":106,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":80,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":111,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"},{\"frame_id\":0,\"type\":\"mismatching_groups\",\"annotation_ids\":[{\"obj_id\":77,\"job_id\":27,\"type\":\"shape\",\"shape_type\":\"rectangle\"},{\"obj_id\":114,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"warning\"}],\"annotations\":{\"valid_count\":21,\"missing_count\":3,\"extra_count\":10,\"total_count\":37,\"ds_count\":34,\"gt_count\":27,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,2,3],[1,21,7],[1,2,0]],\"precision\":[0.0,0.7241379310344828,0.0],\"recall\":[0.0,0.84,0.0],\"accuracy\":[0.0,0.6363636363636364,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.5675675675675675,\"precision\":0.6176470588235294,\"recall\":0.7777777777777778},\"annotation_components\":{\"shape\":{\"valid_count\":24,\"missing_count\":3,\"extra_count\":10,\"total_count\":37,\"ds_count\":34,\"gt_count\":27,\"mean_iou\":0.5859886547819836,\"accuracy\":0.6486486486486487},\"label\":{\"valid_count\":21,\"invalid_count\":3,\"total_count\":24,\"accuracy\":0.875}},\"conflict_count\":31,\"warning_count\":15,\"error_count\":16,\"conflicts_by_type\":{\"low_overlap\":6,\"missing_annotation\":3,\"extra_annotation\":10,\"mismatching_label\":3,\"mismatching_direction\":1,\"covered_annotation\":1,\"mismatching_attributes\":1,\"mismatching_groups\":6}},\"1\":{\"conflicts\":[{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":130,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"rectangle\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":128,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"points\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":124,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polygon\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":125,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"ellipse\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":127,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"mask\"}],\"severity\":\"error\"},{\"frame_id\":1,\"type\":\"missing_annotation\",\"annotation_ids\":[{\"obj_id\":129,\"job_id\":28,\"type\":\"shape\",\"shape_type\":\"polyline\"}],\"severity\":\"error\"}],\"annotations\":{\"valid_count\":0,\"missing_count\":6,\"extra_count\":0,\"total_count\":6,\"ds_count\":0,\"gt_count\":6,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,0,0],[0,0,0],[0,6,0]],\"precision\":[0.0,0.0,0.0],\"recall\":[0.0,0.0,0.0],\"accuracy\":[0.0,0.0,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.0,\"precision\":0.0,\"recall\":0.0},\"annotation_components\":{\"shape\":{\"valid_count\":0,\"missing_count\":6,\"extra_count\":0,\"total_count\":6,\"ds_count\":0,\"gt_count\":6,\"mean_iou\":0.0,\"accuracy\":0.0},\"label\":{\"valid_count\":0,\"invalid_count\":0,\"total_count\":0,\"accuracy\":0.0}},\"conflict_count\":6,\"warning_count\":0,\"error_count\":6,\"conflicts_by_type\":{\"missing_annotation\":6}},\"2\":{\"conflicts\":[],\"annotations\":{\"valid_count\":0,\"missing_count\":0,\"extra_count\":0,\"total_count\":0,\"ds_count\":0,\"gt_count\":0,\"confusion_matrix\":{\"labels\":[\"dog\",\"cat\",\"unmatched\"],\"rows\":[[0,0,0],[0,0,0],[0,0,0]],\"precision\":[0.0,0.0,0.0],\"recall\":[0.0,0.0,0.0],\"accuracy\":[0.0,0.0,0.0],\"axes\":{\"cols\":\"gt\",\"rows\":\"ds\"}},\"accuracy\":0.0,\"precision\":0.0,\"recall\":0.0},\"annotation_components\":{\"shape\":{\"valid_count\":0,\"missing_count\":0,\"extra_count\":0,\"total_count\":0,\"ds_count\":0,\"gt_count\":0,\"mean_iou\":0,\"accuracy\":0.0},\"label\":{\"valid_count\":0,\"invalid_count\":0,\"total_count\":0,\"accuracy\":0.0}},\"conflict_count\":0,\"warning_count\":0,\"error_count\":0,\"conflicts_by_type\":{}}}}" } }, { diff --git a/tests/python/shared/fixtures/init.py b/tests/python/shared/fixtures/init.py index 6b4f720f5b7d..ae849db92031 100644 --- a/tests/python/shared/fixtures/init.py +++ b/tests/python/shared/fixtures/init.py @@ -471,10 +471,10 @@ def collect_code_coverage_from_containers(): # get code coverage report docker_exec(container, "coverage combine", capture_output=False) - docker_exec(container, "coverage xml", capture_output=False) + docker_exec(container, "coverage json", capture_output=False) docker_cp( - f"{PREFIX}_{container}_1:home/django/coverage.xml", - f"coverage_{container}.xml", + f"{PREFIX}_{container}_1:home/django/coverage.json", + f"coverage_{container}.json", ) diff --git a/tests/python/shared/utils/helpers.py b/tests/python/shared/utils/helpers.py index 289d24e7966f..312fb99f66c4 100644 --- a/tests/python/shared/utils/helpers.py +++ b/tests/python/shared/utils/helpers.py @@ -13,9 +13,9 @@ def generate_image_file(filename="image.png", size=(50, 50), color=(0, 0, 0)): f = BytesIO() - image = Image.new("RGB", size=size, color=color) - image.save(f, "jpeg") f.name = filename + image = Image.new("RGB", size=size, color=color) + image.save(f) f.seek(0) return f diff --git a/tests/yarn.lock b/tests/yarn.lock index fca8679089c7..1c2cba350def 100644 --- a/tests/yarn.lock +++ b/tests/yarn.lock @@ -2449,14 +2449,14 @@ sax@>=0.6.0: integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== semver@^6.0.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.3.2: - version "7.3.7" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz" - integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" diff --git a/yarn.lock b/yarn.lock index cbfff391e155..b48ea52b3183 100644 --- a/yarn.lock +++ b/yarn.lock @@ -79,7 +79,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.10.tgz#9d92fa81b87542fff50e848ed585b4212c1d34ec" integrity sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg== -"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.17.9", "@babel/core@^7.4.5", "@babel/core@^7.6.0", "@babel/core@^7.7.5": +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.4.5", "@babel/core@^7.6.0", "@babel/core@^7.7.5": version "7.20.12" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== @@ -1064,10 +1064,259 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@csstools/convert-colors@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" - integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== +"@csstools/cascade-layer-name-parser@^1.0.3", "@csstools/cascade-layer-name-parser@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.4.tgz#3ff490b84660dc0592b4315029f22908f3de0577" + integrity sha512-zXMGsJetbLoXe+gjEES07MEGjL0Uy3hMxmnGtVBrRpVKr5KV9OgCB09zr/vLrsEtoVQTgJFewxaU8IYSAE4tjg== + +"@csstools/color-helpers@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-3.0.0.tgz#b64a9d86663b6d843b169f5da300f78c0242efc2" + integrity sha512-rBODd1rY01QcenD34QxbQxLc1g+Uh7z1X/uzTHNQzJUnFCT9/EZYI7KWq+j0YfWMXJsRJ8lVkqBcB0R/qLr+yg== + +"@csstools/css-calc@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-1.1.3.tgz#75e07eec075f1f3df0ce25575dab3d63da2bd680" + integrity sha512-7mJZ8gGRtSQfQKBQFi5N0Z+jzNC0q8bIkwojP1W0w+APzEqHu5wJoGVsvKxVnVklu9F8tW1PikbBRseYnAdv+g== + +"@csstools/css-color-parser@^1.2.2": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-1.2.3.tgz#0cd0f72c50894a623ae09f19e30bbfb298769f59" + integrity sha512-YaEnCoPTdhE4lPQFH3dU4IEk8S+yCnxS88wMv45JzlnMfZp57hpqA6qf2gX8uv7IJTJ/43u6pTQmhy7hCjlz7g== + dependencies: + "@csstools/color-helpers" "^3.0.0" + "@csstools/css-calc" "^1.1.3" + +"@csstools/css-parser-algorithms@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.0.tgz#0cc3a656dc2d638370ecf6f98358973bfbd00141" + integrity sha512-dTKSIHHWc0zPvcS5cqGP+/TPFUJB0ekJ9dGKvMAFoNuBFhDPBt9OMGNZiIA5vTiNdGHHBeScYPXIGBMnVOahsA== + +"@csstools/css-parser-algorithms@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.1.tgz#ec4fc764ba45d2bb7ee2774667e056aa95003f3a" + integrity sha512-xrvsmVUtefWMWQsGgFffqWSK03pZ1vfDki4IVIIUxxDKnGBzqNgv0A7SB1oXtVNEkcVO8xi1ZrTL29HhSu5kGA== + +"@csstools/css-tokenizer@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz#07ae11a0a06365d7ec686549db7b729bc036528e" + integrity sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA== + +"@csstools/css-tokenizer@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.2.0.tgz#9d70e6dcbe94e44c7400a2929928db35c4de32b5" + integrity sha512-wErmsWCbsmig8sQKkM6pFhr/oPha1bHfvxsUY5CYSQxwyhA9Ulrs8EqCgClhg4Tgg2XapVstGqSVcz0xOYizZA== + +"@csstools/media-query-list-parser@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.2.tgz#6ef642b728d30c1009bfbba3211c7e4c11302728" + integrity sha512-M8cFGGwl866o6++vIY7j1AKuq9v57cf+dGepScwCcbut9ypJNr4Cj+LLTWligYUZ0uyhEoJDKt5lvyBfh2L3ZQ== + +"@csstools/media-query-list-parser@^2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.3.tgz#4471ebd436a22019378fe9c8ac8c0f30c4fbb796" + integrity sha512-ATul1u+pic4aVpstgueqxEv4MsObEbszAxfTXpx9LHaeD3LAh+wFqdCteyegWmjk0k5rkSCAvIOaJe9U3DD09w== + +"@csstools/postcss-cascade-layers@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.0.tgz#21f8556de640f9f9ccfb950c49a886280fe5497e" + integrity sha512-dVPVVqQG0FixjM9CG/+8eHTsCAxRKqmNh6H69IpruolPlnEF1611f2AoLK8TijTSAsqBSclKd4WHs1KUb/LdJw== + dependencies: + "@csstools/selector-specificity" "^3.0.0" + postcss-selector-parser "^6.0.13" + +"@csstools/postcss-color-function@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-3.0.1.tgz#2f688783f9e8b2496bd0df6edbfb47b8300f01af" + integrity sha512-+vrvCQeUifpMeyd42VQ3JPWGQ8cO19+TnGbtfq1SDSgZzRapCQO4aK9h/jhMOKPnxGzbA57oS0aHgP/12N9gSQ== + dependencies: + "@csstools/css-color-parser" "^1.2.2" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + "@csstools/postcss-progressive-custom-properties" "^3.0.0" + +"@csstools/postcss-color-mix-function@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-2.0.1.tgz#51c5656bcbee9d02d00d10ddcdb0a55486573fd4" + integrity sha512-Z5cXkLiccKIVcUTe+fAfjUD7ZUv0j8rq3dSoBpM6I49dcw+50318eYrwUZa3nyb4xNx7ntNNUPmesAc87kPE2Q== + dependencies: + "@csstools/css-color-parser" "^1.2.2" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + "@csstools/postcss-progressive-custom-properties" "^3.0.0" + +"@csstools/postcss-exponential-functions@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-1.0.0.tgz#2e558ad2856e0c737d9cb98a5d91cfe8d785c9f6" + integrity sha512-FPndJ/7oGlML7/4EhLi902wGOukO0Nn37PjwOQGc0BhhjQPy3np3By4d3M8s9Cfmp9EHEKgUHRN2DQ5HLT/hTw== + dependencies: + "@csstools/css-calc" "^1.1.3" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + +"@csstools/postcss-font-format-keywords@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-3.0.0.tgz#9ca3a3ca67122862addf8a1c0c61a6db02dea1cc" + integrity sha512-ntkGj+1uDa/u6lpjPxnkPcjJn7ChO/Kcy08YxctOZI7vwtrdYvFhmE476dq8bj1yna306+jQ9gzXIG/SWfOaRg== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-gradients-interpolation-method@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-4.0.1.tgz#abbe5ec9992b850c4330da2f1b57e73d2f5f5086" + integrity sha512-IHeFIcksjI8xKX7PWLzAyigM3UvJdZ4btejeNa7y/wXxqD5dyPPZuY55y8HGTrS6ETVTRqfIznoCPtTzIX7ygQ== + dependencies: + "@csstools/css-color-parser" "^1.2.2" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + "@csstools/postcss-progressive-custom-properties" "^3.0.0" + +"@csstools/postcss-hwb-function@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-3.0.1.tgz#72f47fae09e0dc48be4bd94cab15e6e98cc6de00" + integrity sha512-FYe2K8EOYlL1BUm2HTXVBo6bWAj0xl4khOk6EFhQHy/C5p3rlr8OcetzQuwMeNQ3v25nB06QTgqUHoOUwoEqhA== + dependencies: + "@csstools/css-color-parser" "^1.2.2" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + +"@csstools/postcss-ic-unit@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-3.0.0.tgz#bbc55170d880daa3cc096ee160e8f2492a48e881" + integrity sha512-FH3+zfOfsgtX332IIkRDxiYLmgwyNk49tfltpC6dsZaO4RV2zWY6x9VMIC5cjvmjlDO7DIThpzqaqw2icT8RbQ== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^3.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-is-pseudo-class@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-4.0.0.tgz#954c489cf207a7cfeaf4d96d39fac50757dc48cf" + integrity sha512-0I6siRcDymG3RrkNTSvHDMxTQ6mDyYE8awkcaHNgtYacd43msl+4ZWDfQ1yZQ/viczVWjqJkLmPiRHSgxn5nZA== + dependencies: + "@csstools/selector-specificity" "^3.0.0" + postcss-selector-parser "^6.0.13" + +"@csstools/postcss-logical-float-and-clear@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-2.0.0.tgz#15e1b5d16dce01ad1e676167d0909e3958234eb5" + integrity sha512-Wki4vxsF6icRvRz8eF9bPpAvwaAt0RHwhVOyzfoFg52XiIMjb6jcbHkGxwpJXP4DVrnFEwpwmrz5aTRqOW82kg== + +"@csstools/postcss-logical-resize@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-resize/-/postcss-logical-resize-2.0.0.tgz#751bd5aab335c9973e346e3edacb2a0a16fa8296" + integrity sha512-lCQ1aX8c5+WI4t5EoYf3alTzJNNocMqTb+u1J9CINdDhFh1fjovqK+0aHalUHsNstZmzFPNzIkU4Mb3eM9U8SA== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-logical-viewport-units@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-2.0.1.tgz#2921034d11d60ea7340ebe795bb4fe60f32ebbae" + integrity sha512-R5s19SscS7CHoxvdYNMu2Y3WDwG4JjdhsejqjunDB1GqfzhtHSvL7b5XxCkUWqm2KRl35hI6kJ4HEaCDd/3BXg== + dependencies: + "@csstools/css-tokenizer" "^2.2.0" + +"@csstools/postcss-media-minmax@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-minmax/-/postcss-media-minmax-1.0.6.tgz#ed4cac86640c2dd4c3aa2ec491afc28527a10151" + integrity sha512-BmwKkqEzzQz6D+5ctoacsiGrq4kVgd1PMEPwkwdR0qFaL2C2nguGsWG87xEw+HIts/2yxhIPTm7Jp3DQq+wn3Q== + dependencies: + "@csstools/css-calc" "^1.1.3" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + "@csstools/media-query-list-parser" "^2.1.3" + +"@csstools/postcss-media-queries-aspect-ratio-number-values@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-2.0.1.tgz#00d30e83e0b1e82a09ab890dcef80cc41be1ab8c" + integrity sha512-UvMYxXT3R011whbxzRwLx7d7eNGyVsnZo7waAmf10ZGnT34XidY+rsdFnk6OdFwuG6FYqw3/tptQEAZOmUgvLw== + dependencies: + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + "@csstools/media-query-list-parser" "^2.1.3" + +"@csstools/postcss-nested-calc@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-nested-calc/-/postcss-nested-calc-3.0.0.tgz#b9069f5e1c2ea08de3840a5922e39af4e0ecf4b1" + integrity sha512-HsB66aDWAouOwD/GcfDTS0a7wCuVWaTpXcjl5VKP0XvFxDiU+r0T8FG7xgb6ovZNZ+qzvGIwRM+CLHhDgXrYgQ== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-normalize-display-values@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-3.0.0.tgz#de995eeafe217ac1854a7135b1db44c57487e9ea" + integrity sha512-6Nw55PRXEKEVqn3bzA8gRRPYxr5tf5PssvcE5DRA/nAxKgKtgNZMCHCSd1uxTCWeyLnkf6h5tYRSB0P1Vh/K/A== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-oklab-function@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-3.0.1.tgz#2e33ed1761ce78d59a9156f1201a52fda7c75899" + integrity sha512-3TIz+dCPlQPzz4yAEYXchUpfuU2gRYK4u1J+1xatNX85Isg4V+IbLyppblWLV4Vb6npFF8qsHN17rNuxOIy/6w== + dependencies: + "@csstools/css-color-parser" "^1.2.2" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + "@csstools/postcss-progressive-custom-properties" "^3.0.0" + +"@csstools/postcss-progressive-custom-properties@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-3.0.0.tgz#bb86ae4bb7f2206b0cf6e9b8f0bfc191f67271d8" + integrity sha512-2/D3CCL9DN2xhuUTP8OKvKnaqJ1j4yZUxuGLsCUOQ16wnDAuMLKLkflOmZF5tsPh/02VPeXRmqIN+U595WAulw== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-relative-color-syntax@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-2.0.1.tgz#b7e928fdef9366e1060e2bf4d95cab605855446b" + integrity sha512-9B8br/7q0bjD1fV3yE22izjc7Oy5hDbDgwdFEz207cdJHYC9yQneJzP3H+/w3RgC7uyfEVhyyhkGRx5YAfJtmg== + dependencies: + "@csstools/css-color-parser" "^1.2.2" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + "@csstools/postcss-progressive-custom-properties" "^3.0.0" + +"@csstools/postcss-scope-pseudo-class@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-3.0.0.tgz#23f32181b7de9a33e7c7c71f7620b78284955b82" + integrity sha512-GFNVsD97OuEcfHmcT0/DAZWAvTM/FFBDQndIOLawNc1Wq8YqpZwBdHa063Lq+Irk7azygTT+Iinyg3Lt76p7rg== + dependencies: + postcss-selector-parser "^6.0.13" + +"@csstools/postcss-stepped-value-functions@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-3.0.1.tgz#c337a8ae09bec13cdf6c95f63a58b407f6965557" + integrity sha512-y1sykToXorFE+5cjtp//xAMWEAEple0kcZn2QhzEFIZDDNvGOCp5JvvmmPGsC3eDlj6yQp70l9uXZNLnimEYfA== + dependencies: + "@csstools/css-calc" "^1.1.3" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + +"@csstools/postcss-text-decoration-shorthand@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-3.0.0.tgz#468800a47fcb4760df8c60bbf1ba7999f44b4dd4" + integrity sha512-BAa1MIMJmEZlJ+UkPrkyoz3DC7kLlIl2oDya5yXgvUrelpwxddgz8iMp69qBStdXwuMyfPx46oZcSNx8Z0T2eA== + dependencies: + "@csstools/color-helpers" "^3.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-trigonometric-functions@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-3.0.1.tgz#06148aa8624b69a6573adb40ed27d3d019875caa" + integrity sha512-hW+JPv0MPQfWC1KARgvJI6bisEUFAZWSvUNq/khGCupYV/h6Z9R2ZFz0Xc633LXBst0ezbXpy7NpnPurSx5Klw== + dependencies: + "@csstools/css-calc" "^1.1.3" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + +"@csstools/postcss-unset-value@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-unset-value/-/postcss-unset-value-3.0.0.tgz#6d2f08140b41d3e70d805ccd2baaf64a6f59fdac" + integrity sha512-P0JD1WHh3avVyKKRKjd0dZIjCEeaBer8t1BbwGMUDtSZaLhXlLNBqZ8KkqHzYWXOJgHleXAny2/sx8LYl6qhEA== + +"@csstools/selector-specificity@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-3.0.0.tgz#798622546b63847e82389e473fd67f2707d82247" + integrity sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g== "@ctrl/tinycolor@^3.4.0": version "3.5.0" @@ -1395,6 +1644,11 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@kurkle/color@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f" + integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -1518,21 +1772,6 @@ dependencies: "@sinonjs/commons" "^2.0.0" -"@stylelint/postcss-css-in-js@^0.37.2": - version "0.37.3" - resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.3.tgz#d149a385e07ae365b0107314c084cb6c11adbf49" - integrity sha512-scLk3cSH1H9KggSniseb2KNAU5D9FWc3H7BxCSAIdtU9OWIyw0zkEZ9qEKHryRM+SExYXRKNb7tOOVNAsQ3iwg== - dependencies: - "@babel/core" "^7.17.9" - -"@stylelint/postcss-markdown@^0.36.2": - version "0.36.2" - resolved "https://registry.yarnpkg.com/@stylelint/postcss-markdown/-/postcss-markdown-0.36.2.tgz#0a540c4692f8dcdfc13c8e352c17e7bfee2bb391" - integrity sha512-2kGbqUVJUGE8dM+bMzXG/PYUWKkjLIkRLWNh39OaADkiabDRdw8ATFCgbMz5xdIcvwspPAluSL7uY+ZiTWdWmQ== - dependencies: - remark "^13.0.0" - unist-util-find-all-after "^3.0.2" - "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -1789,7 +2028,7 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== -"@types/minimist@^1.2.0": +"@types/minimist@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== @@ -1819,11 +2058,6 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== - "@types/parse5@^6.0.0": version "6.0.3" resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" @@ -1878,9 +2112,9 @@ "@types/reactcss" "*" "@types/react-dom@^16.9.14", "@types/react-dom@^18.0.5": - version "18.2.6" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.6.tgz#ad621fa71a8db29af7c31b41b2ea3d8a6f4144d1" - integrity sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A== + version "18.2.7" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.7.tgz#67222a08c0a6ae0a0da33c3532348277c70abb63" + integrity sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA== dependencies: "@types/react" "*" @@ -2439,11 +2673,6 @@ airbnb@0.0.2: dependencies: chalk "^2.4.2" -ajv-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" - integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== - ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -2451,7 +2680,7 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" -ajv-keywords@^3.1.0, ajv-keywords@^3.5.2: +ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== @@ -2463,7 +2692,7 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2642,6 +2871,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + aria-query@^5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" @@ -2754,18 +2988,17 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -autoprefixer@^9.6.1, autoprefixer@^9.8.6: - version "9.8.8" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.8.tgz#fd4bd4595385fa6f06599de749a4d5f7a474957a" - integrity sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA== +autoprefixer@^10.4.14: + version "10.4.14" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d" + integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== dependencies: - browserslist "^4.12.0" - caniuse-lite "^1.0.30001109" + browserslist "^4.21.5" + caniuse-lite "^1.0.30001464" + fraction.js "^4.2.0" normalize-range "^0.1.2" - num2fraction "^1.2.2" - picocolors "^0.2.1" - postcss "^7.0.32" - postcss-value-parser "^4.1.0" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" available-typed-arrays@^1.0.5: version "1.0.5" @@ -3133,7 +3366,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.6.4: +browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^4.21.4: version "4.21.4" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== @@ -3143,6 +3376,16 @@ browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^ node-releases "^2.0.6" update-browserslist-db "^1.0.9" +browserslist@^4.21.5, browserslist@^4.21.9: + version "4.21.9" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.9.tgz#e11bdd3c313d7e2a9e87e8b4b0c7872b13897635" + integrity sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg== + dependencies: + caniuse-lite "^1.0.30001503" + electron-to-chromium "^1.4.431" + node-releases "^2.0.12" + update-browserslist-db "^1.0.11" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -3210,25 +3453,6 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ== - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A== - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ== - callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -3242,21 +3466,22 @@ camel-case@^4.1.2: pascal-case "^3.1.2" tslib "^2.0.3" -camelcase-keys@^6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" - integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== +camelcase-keys@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-7.0.2.tgz#d048d8c69448745bb0de6fc4c1c52a30dfbe7252" + integrity sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg== dependencies: - camelcase "^5.3.1" - map-obj "^4.0.0" - quick-lru "^4.0.1" + camelcase "^6.3.0" + map-obj "^4.1.0" + quick-lru "^5.1.1" + type-fest "^1.2.1" camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.2.0: +camelcase@^6.2.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -3266,11 +3491,16 @@ camera-controls@^1.25.3: resolved "https://registry.yarnpkg.com/camera-controls/-/camera-controls-1.37.5.tgz#e5499840ee0beebf5c61dab03ed6386a11c8708e" integrity sha512-QpSJnPkNyfMchXP589laVcc/STHTWpDo546MGRJk5eFoBXAa5sWkTXGGdTvZ+WqfXZDVKaRiDUv1dFBES2dxiw== -caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001400: +caniuse-lite@^1.0.30001400: version "1.0.30001445" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001445.tgz#cf2d4eb93f2bcdf0310de9dd6d18be271bc0b447" integrity sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg== +caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001503: + version "1.0.30001517" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz#90fabae294215c3495807eb24fc809e11dc2f0a8" + integrity sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA== + canvas@^2.8.0: version "2.11.0" resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.11.0.tgz#7f0c3e9ae94cf469269b5d3a7963a7f3a9936434" @@ -3318,7 +3548,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: +chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3366,6 +3596,13 @@ character-reference-invalid@^2.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== +chart.js@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.3.0.tgz#ac363030ab3fec572850d2d872956f32a46326a1" + integrity sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g== + dependencies: + "@kurkle/color" "^0.3.0" + check-links@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/check-links/-/check-links-1.1.8.tgz#842184178c85d9c2ab119175bcc2672681bc88a4" @@ -3478,13 +3715,6 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" -clone-regexp@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f" - integrity sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q== - dependencies: - is-regexp "^2.0.0" - clone-response@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" @@ -3560,6 +3790,11 @@ color-support@^1.1.2: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== +colord@^2.9.3: + version "2.9.3" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" + integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== + colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.19: version "2.0.19" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" @@ -3743,26 +3978,15 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cosmiconfig@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - -cosmiconfig@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" - integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== +cosmiconfig@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd" + integrity sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ== dependencies: - "@types/parse-json" "^4.0.0" import-fresh "^3.2.1" + js-yaml "^4.1.0" parse-json "^5.0.0" path-type "^4.0.0" - yaml "^1.10.0" cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" @@ -3773,46 +3997,45 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -css-blank-pseudo@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" - integrity sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w== +css-blank-pseudo@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-6.0.0.tgz#2bc6f812a5f60296c04c55b1696bad4300dcdbcc" + integrity sha512-VbfLlOWO7sBHBTn6pwDQzc07Z0SDydgDBfNfCE0nvrehdBNv9RKsuupIRa/qal0+fBZhAALyQDPMKz5lnvcchw== dependencies: - postcss "^7.0.5" + postcss-selector-parser "^6.0.13" -css-has-pseudo@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee" - integrity sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^5.0.0-rc.4" +css-functions-list@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.0.tgz#8290b7d064bf483f48d6559c10e98dc4d1ad19ee" + integrity sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg== -css-loader@^3.4.2: - version "3.6.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645" - integrity sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ== +css-has-pseudo@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-6.0.0.tgz#b8c8f39a19bc83c5be59fd251510a7e443c47968" + integrity sha512-X+r+JBuoO37FBOWVNhVJhxtSBUFHgHbrcc0CjFT28JEdOw1qaDwABv/uunyodUuSy2hMPe9j/HjssxSlvUmKjg== dependencies: - camelcase "^5.3.1" - cssesc "^3.0.0" - icss-utils "^4.1.1" - loader-utils "^1.2.3" - normalize-path "^3.0.0" - postcss "^7.0.32" - postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^3.0.2" - postcss-modules-scope "^2.2.0" - postcss-modules-values "^3.0.0" - postcss-value-parser "^4.1.0" - schema-utils "^2.7.0" - semver "^6.3.0" + "@csstools/selector-specificity" "^3.0.0" + postcss-selector-parser "^6.0.13" + postcss-value-parser "^4.2.0" -css-prefers-color-scheme@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4" - integrity sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg== - dependencies: - postcss "^7.0.5" +css-loader@^6.8.1: + version "6.8.1" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88" + integrity sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.21" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.3" + postcss-modules-scope "^3.0.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.2.0" + semver "^7.3.8" + +css-prefers-color-scheme@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-9.0.0.tgz#7e9b74062655ea15490e359cb456a3b9f4c93327" + integrity sha512-03QGAk/FXIRseDdLb7XAiu6gidQ0Nd8945xuM7VFVPpc6goJsG9uIO8xQjTxwbPdPIIV4o4AJoOJyt8gwDl67g== css-select-base-adapter@^0.1.1: version "0.1.1" @@ -3861,6 +4084,14 @@ css-tree@^1.1.2: mdn-data "2.0.14" source-map "^0.6.1" +css-tree@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== + dependencies: + mdn-data "2.0.30" + source-map-js "^1.0.1" + css-what@^3.2.1: version "3.4.2" resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" @@ -3871,15 +4102,10 @@ css-what@^6.0.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== -cssdb@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" - integrity sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ== - -cssesc@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" - integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== +cssdb@^7.7.0: + version "7.7.0" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.7.0.tgz#8a62f1c825c019134e7830729f050c29e3eca95e" + integrity sha512-1hN+I3r4VqSNQ+OmMXxYexnumbOONkSil0TWMebVXHtzYW4tRRPovUNHPHj2d4nrgOuYJ8Vs3XwvywsuwwXNNA== cssesc@^3.0.0: version "3.0.0" @@ -3929,7 +4155,7 @@ custom-error-instance@2.1.1: three "^0.126.1" "cvat-canvas@link:./cvat-canvas": - version "2.17.0" + version "2.17.1" dependencies: "@types/fabric" "^4.5.7" "@types/polylabel" "^1.0.5" @@ -3942,7 +4168,7 @@ custom-error-instance@2.1.1: svg.select.js "3.0.1" "cvat-core@link:./cvat-core": - version "9.2.0" + version "9.3.0" dependencies: "@types/lodash" "^4.14.191" axios "^0.27.2" @@ -4023,6 +4249,11 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decamelize@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.1.tgz#db11a92e58c741ef339fb0a2868d8a06a9a7b1e9" + integrity sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA== + decimal.js@^10.3.1, decimal.js@^10.4.2: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" @@ -4265,7 +4496,7 @@ dom-serializer@^1.0.1: domhandler "^4.2.0" entities "^2.0.0" -domelementtype@1, domelementtype@^1.3.1: +domelementtype@1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== @@ -4282,13 +4513,6 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" @@ -4296,7 +4520,7 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" -domutils@^1.5.1, domutils@^1.7.0: +domutils@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== @@ -4368,6 +4592,11 @@ electron-to-chromium@^1.4.251: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== +electron-to-chromium@^1.4.431: + version "1.4.470" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.470.tgz#0e932816be8d5f2b491ad2aa449ea47db4785398" + integrity sha512-zZM48Lmy2FKWgqyvsX9XK+J6FfP7aCDUFLmgooLJzA7v1agCs/sxSoBpTIwDLhmbhpx9yJIxj2INig/ncjJRqg== + emittery@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" @@ -4415,11 +4644,6 @@ enquirer@^2.3.5: dependencies: ansi-colors "^4.1.1" -entities@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -4894,13 +5118,6 @@ execa@^6.1.0: signal-exit "^3.0.7" strip-final-newline "^3.0.0" -execall@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45" - integrity sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow== - dependencies: - clone-regexp "^2.1.0" - exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -4972,10 +5189,10 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.11, fast-glob@^3.2.5, fast-glob@^3.2.9: - version "3.2.12" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== +fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.0.tgz#7c40cb491e1e2ed5664749e87bfb516dbe8727c0" + integrity sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -4993,7 +5210,7 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fastest-levenshtein@^1.0.12: +fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16: version "1.0.16" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== @@ -5089,6 +5306,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -5107,11 +5332,6 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -flatten@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" - integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== - follow-redirects@^1.0.0, follow-redirects@^1.14.9: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" @@ -5151,6 +5371,11 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== +fraction.js@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" + integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== + fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -5252,11 +5477,6 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-stdin@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" - integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== - get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -5388,13 +5608,6 @@ globjoin@^0.1.4: resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg== -gonzales-pe@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3" - integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ== - dependencies: - minimist "^1.2.5" - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -5667,11 +5880,6 @@ home-or-tmp@^2.0.0: os-homedir "^1.0.0" os-tmpdir "^1.0.1" -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - hosted-git-info@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" @@ -5719,10 +5927,10 @@ html-minifier-terser@^6.0.2: relateurl "^0.2.7" terser "^5.10.0" -html-tags@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961" - integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg== +html-tags@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" + integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== html-void-elements@^2.0.0: version "2.0.1" @@ -5740,18 +5948,6 @@ html-webpack-plugin@^5.5.0: pretty-error "^4.0.0" tapable "^2.0.0" -htmlparser2@^3.10.0: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - htmlparser2@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" @@ -5864,12 +6060,10 @@ iconv-lite@0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -icss-utils@^4.0.0, icss-utils@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" - integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== - dependencies: - postcss "^7.0.14" +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== ignore-by-default@^1.0.1: version "1.0.1" @@ -5881,7 +6075,7 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.0.0, ignore@^5.1.8, ignore@^5.2.0: +ignore@^5.0.0, ignore@^5.2.0, ignore@^5.2.4: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== @@ -5901,21 +6095,6 @@ immutable@^4.0.0: resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.2.tgz#2da9ff4384a4330c36d4d1bc88e90f9e0b0ccd16" integrity sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og== -import-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" - integrity sha512-Ew5AZzJQFqrOV5BTW3EIoHAnoie1LojZLXKcCQ/yTRyVZosBhK1x1ViYjHGf5pAFOq8ZyChZp6m/fSN7pJyZtg== - dependencies: - import-from "^2.1.0" - -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg== - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" - import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -5924,13 +6103,6 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-from@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" - integrity sha512-0vdnLL2wSGnhlRmzHJAg5JHjt1l2vYhzJ7tNLGbeVg0fse56tpGaH0uzH+r9Slej+BSXXEHvBKDEnVSLLE9/+w== - dependencies: - resolve-from "^3.0.0" - import-lazy@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" @@ -5954,10 +6126,10 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA== +indent-string@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" + integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== inflight@^1.0.4: version "1.0.6" @@ -6132,11 +6304,6 @@ is-decimal@^2.0.0: resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw== - is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" @@ -6255,6 +6422,11 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-potential-custom-element-name@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" @@ -6268,11 +6440,6 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-regexp@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d" - integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA== - is-relative-url@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-relative-url/-/is-relative-url-2.0.0.tgz#72902d7fe04b3d4792e7db15f9db84b7204c9cef" @@ -6332,11 +6499,6 @@ is-typedarray@^1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - is-weakmap@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" @@ -6876,6 +7038,11 @@ jest@^29.5.0: import-local "^3.0.2" jest-cli "^29.5.0" +jiti@^1.18.2: + version "1.19.1" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.1.tgz#fa99e4b76a23053e0e7cde098efe1704a14c16f1" + integrity sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg== + js-base64@^3.7.2: version "3.7.4" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.4.tgz#af95b20f23efc8034afd2d1cc5b9d0adf7419037" @@ -6904,6 +7071,13 @@ js-yaml@^3.13.1, js-yaml@^3.6.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + jsdom@^19.0.0: version "19.0.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-19.0.0.tgz#93e67c149fe26816d38a849ea30ac93677e16b6a" @@ -6994,11 +7168,6 @@ json-logic-js@^2.0.1, json-logic-js@^2.0.2: resolved "https://registry.yarnpkg.com/json-logic-js/-/json-logic-js-2.0.2.tgz#b613e095f5e598cb78f7b9a2bbf638e74cf98158" integrity sha512-ZBtBdMJieqQcH7IX/LaBsr5pX+Y5JIW+EhejtM3Ffg2jdN9Iwf+Ht6TbHnvAZ/YtwyuhPaCBlnvzrwVeWdvGDQ== -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -7095,10 +7264,10 @@ klona@^2.0.4: resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== -known-css-properties@^0.21.0: - version "0.21.0" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.21.0.tgz#15fbd0bbb83447f3ce09d8af247ed47c68ede80d" - integrity sha512-sZLUnTqimCkvkgRS+kbPlYW5o8q5w1cu+uIisKpEWkj31I8mx8kNG162DwRav8Zirkva6N5uoFsm9kzK4mUXjw== +known-css-properties@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.27.0.tgz#82a9358dda5fe7f7bd12b5e7142c0a205393c0c5" + integrity sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg== language-subtag-registry@~0.3.2: version "0.3.22" @@ -7205,7 +7374,7 @@ loader-runner@^4.2.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== -loader-utils@^1.1.0, loader-utils@^1.2.3: +loader-utils@^1.2.3: version "1.4.2" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== @@ -7238,6 +7407,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash-es@^4.17.15: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" @@ -7338,14 +7514,6 @@ lodash@^4.0.1, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - log-update@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" @@ -7453,7 +7621,7 @@ map-obj@^1.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg== -map-obj@^4.0.0: +map-obj@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== @@ -7735,6 +7903,11 @@ mdn-data@2.0.14: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== +mdn-data@2.0.30: + version "2.0.30" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== + mdn-data@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" @@ -7766,23 +7939,23 @@ memoize-one@^6.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== -meow@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" - integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ== +meow@^10.1.5: + version "10.1.5" + resolved "https://registry.yarnpkg.com/meow/-/meow-10.1.5.tgz#be52a1d87b5f5698602b0f32875ee5940904aa7f" + integrity sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw== dependencies: - "@types/minimist" "^1.2.0" - camelcase-keys "^6.2.2" - decamelize "^1.2.0" + "@types/minimist" "^1.2.2" + camelcase-keys "^7.0.0" + decamelize "^5.0.0" decamelize-keys "^1.1.0" hard-rejection "^2.1.0" minimist-options "4.1.0" - normalize-package-data "^3.0.0" - read-pkg-up "^7.0.1" - redent "^3.0.0" - trim-newlines "^3.0.0" - type-fest "^0.18.0" - yargs-parser "^20.2.3" + normalize-package-data "^3.0.2" + read-pkg-up "^8.0.0" + redent "^4.0.0" + trim-newlines "^4.0.2" + type-fest "^1.2.2" + yargs-parser "^20.2.9" merge-descriptors@1.0.1: version "1.0.1" @@ -8188,7 +8361,7 @@ mimic-response@^2.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== -min-indent@^1.0.0: +min-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== @@ -8214,7 +8387,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== @@ -8296,6 +8469,11 @@ nan@^2.17.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -8348,6 +8526,11 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" +node-releases@^2.0.12: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + node-releases@^2.0.6: version "2.0.8" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae" @@ -8383,17 +8566,7 @@ nopt@~1.0.10: dependencies: abbrev "1" -normalize-package-data@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-package-data@^3.0.0: +normalize-package-data@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== @@ -8413,11 +8586,6 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== -normalize-selector@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03" - integrity sha512-dxvWdI8gw6eAvk9BlPffgEoGfM7AdijoCwOEJge3e3ulT2XLgmU7KvvxprOaCu05Q1uGRHmOhHe1r6emZoKyFw== - normalize-url@^4.1.0: version "4.5.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" @@ -8466,11 +8634,6 @@ nth-check@^2.0.0, nth-check@^2.0.1: dependencies: boolbase "^1.0.0" -num2fraction@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" - integrity sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg== - nwsapi@^2.2.0, nwsapi@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" @@ -8733,7 +8896,7 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.1.0: +p-limit@^3.0.2, p-limit@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== @@ -8754,6 +8917,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" @@ -8865,14 +9035,6 @@ parse-entities@^4.0.0: is-decimal "^2.0.0" is-hexadecimal "^2.0.0" -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw== - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -8960,11 +9122,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -picocolors@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" - integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -9014,412 +9171,341 @@ polylabel@^1.1.0: dependencies: tinyqueue "^2.0.3" -postcss-attribute-case-insensitive@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz#d93e46b504589e94ac7277b0463226c68041a880" - integrity sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA== +postcss-attribute-case-insensitive@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-6.0.2.tgz#e843091859323342e461878d201ee70278809e01" + integrity sha512-IRuCwwAAQbgaLhxQdQcIIK0dCVXg3XDUnzgKD8iwdiYdwU4rMWRWyl/W9/0nA4ihVpq5pyALiHB2veBJ0292pw== dependencies: - postcss "^7.0.2" - postcss-selector-parser "^6.0.2" + postcss-selector-parser "^6.0.10" -postcss-color-functional-notation@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz#5efd37a88fbabeb00a2966d1e53d98ced93f74e0" - integrity sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g== +postcss-clamp@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz#7263e95abadd8c2ba1bd911b0b5a5c9c93e02363" + integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" + postcss-value-parser "^4.2.0" -postcss-color-gray@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz#532a31eb909f8da898ceffe296fdc1f864be8547" - integrity sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw== +postcss-color-functional-notation@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-6.0.0.tgz#dcc1b8b6273099c597a790dc484d89e2573f5f17" + integrity sha512-kaWTgnhRKFtfMF8H0+NQBFxgr5CGg05WGe07Mc1ld6XHwwRWlqSbHOW0zwf+BtkBQpsdVUu7+gl9dtdvhWMedw== dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.5" - postcss-values-parser "^2.0.0" + "@csstools/postcss-progressive-custom-properties" "^3.0.0" + postcss-value-parser "^4.2.0" -postcss-color-hex-alpha@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz#a8d9ca4c39d497c9661e374b9c51899ef0f87388" - integrity sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw== +postcss-color-hex-alpha@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-9.0.2.tgz#6d3ed50342802469880981a1999515d003ff7d79" + integrity sha512-SfPjgr//VQ/DOCf80STIAsdAs7sbIbxATvVmd+Ec7JvR8onz9pjawhq3BJM3Pie40EE3TyB0P6hft16D33Nlyg== dependencies: - postcss "^7.0.14" - postcss-values-parser "^2.0.1" + postcss-value-parser "^4.2.0" -postcss-color-mod-function@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz#816ba145ac11cc3cb6baa905a75a49f903e4d31d" - integrity sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ== +postcss-color-rebeccapurple@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-9.0.0.tgz#317bf718962c70b779efacf3dc040c56f05d03ce" + integrity sha512-RmUFL+foS05AKglkEoqfx+KFdKRVmqUAxlHNz4jLqIi7046drIPyerdl4B6j/RA2BSP8FI8gJcHmLRrwJOMnHw== dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.2" - postcss-values-parser "^2.0.0" + postcss-value-parser "^4.2.0" -postcss-color-rebeccapurple@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz#c7a89be872bb74e45b1e3022bfe5748823e6de77" - integrity sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g== +postcss-custom-media@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-10.0.0.tgz#299781f67d043de7d3eaa13923c26c586d9cd57a" + integrity sha512-NxDn7C6GJ7X8TsWOa8MbCdq9rLERRLcPfQSp856k1jzMreL8X9M6iWk35JjPRIb9IfRnVohmxAylDRx7n4Rv4g== + dependencies: + "@csstools/cascade-layer-name-parser" "^1.0.3" + "@csstools/css-parser-algorithms" "^2.3.0" + "@csstools/css-tokenizer" "^2.1.1" + "@csstools/media-query-list-parser" "^2.1.2" + +postcss-custom-properties@^13.3.0: + version "13.3.0" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-13.3.0.tgz#53361280a9ec57c2ac448c0877ba25c4978241ee" + integrity sha512-q4VgtIKSy5+KcUvQ0WxTjDy9DZjQ5VCXAZ9+tT9+aPMbA0z6s2t1nMw0QHszru1ib5ElkXl9JUpYYU37VVUs7g== + dependencies: + "@csstools/cascade-layer-name-parser" "^1.0.4" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + postcss-value-parser "^4.2.0" + +postcss-custom-selectors@^7.1.4: + version "7.1.4" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-7.1.4.tgz#5980972353119af0d9725bdcccad46be8cfc9011" + integrity sha512-TU2xyUUBTlpiLnwyE2ZYMUIYB41MKMkBZ8X8ntkqRDQ8sdBLhFFsPgNcOliBd5+/zcK51C9hRnSE7hKUJMxQSw== + dependencies: + "@csstools/cascade-layer-name-parser" "^1.0.3" + "@csstools/css-parser-algorithms" "^2.3.0" + "@csstools/css-tokenizer" "^2.1.1" + postcss-selector-parser "^6.0.13" + +postcss-dir-pseudo-class@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-8.0.0.tgz#9e4e37d170f672520d3f38fd8376db0ca04d4e9c" + integrity sha512-Oy5BBi0dWPwij/IA+yDYj+/OBMQ9EPqAzTHeSNUYrUWdll/PRJmcbiUj0MNcsBi681I1gcSTLvMERPaXzdbvJg== dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" + postcss-selector-parser "^6.0.13" -postcss-custom-media@^7.0.8: - version "7.0.8" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz#fffd13ffeffad73621be5f387076a28b00294e0c" - integrity sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg== +postcss-double-position-gradients@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-5.0.0.tgz#cdc11e1210c3fbd3f7bc242a5ee83e5b9d7db8fa" + integrity sha512-wR8npIkrIVUTicUpCWSSo1f/g7gAEIH70FMqCugY4m4j6TX4E0T2Q5rhfO0gqv00biBZdLyb+HkW8x6as+iJNQ== dependencies: - postcss "^7.0.14" + "@csstools/postcss-progressive-custom-properties" "^3.0.0" + postcss-value-parser "^4.2.0" -postcss-custom-properties@^8.0.11: - version "8.0.11" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz#2d61772d6e92f22f5e0d52602df8fae46fa30d97" - integrity sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA== +postcss-focus-visible@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-9.0.0.tgz#a81227428d6f1e524099c6581f7c7132f987e382" + integrity sha512-zA4TbVaIaT8npZBEROhZmlc+GBKE8AELPHXE7i4TmIUEQhw/P/mSJfY9t6tBzpQ1rABeGtEOHYrW4SboQeONMQ== dependencies: - postcss "^7.0.17" - postcss-values-parser "^2.0.1" + postcss-selector-parser "^6.0.13" -postcss-custom-selectors@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz#64858c6eb2ecff2fb41d0b28c9dd7b3db4de7fba" - integrity sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w== +postcss-focus-within@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-8.0.0.tgz#8304380dd2dadc1c2dcfa52816ff86be7736fc16" + integrity sha512-E7+J9nuQzZaA37D/MUZMX1K817RZGDab8qw6pFwzAkDd/QtlWJ9/WTKmzewNiuxzeq6WWY7ATiRePVoDKp+DnA== dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" + postcss-selector-parser "^6.0.13" -postcss-dir-pseudo-class@^5.0.0: +postcss-font-variant@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz#6e3a4177d0edb3abcc85fdb6fbb1c26dabaeaba2" - integrity sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" - -postcss-double-position-gradients@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz#fc927d52fddc896cb3a2812ebc5df147e110522e" - integrity sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA== - dependencies: - postcss "^7.0.5" - postcss-values-parser "^2.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz#efd59b4b7ea8bb06127f2d031bfbb7f24d32fa66" + integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== -postcss-env-function@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-2.0.2.tgz#0f3e3d3c57f094a92c2baf4b6241f0b0da5365d7" - integrity sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-focus-visible@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e" - integrity sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g== - dependencies: - postcss "^7.0.2" +postcss-gap-properties@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-5.0.0.tgz#3bd77f3d51facb1da404b4edd72b8203929385a5" + integrity sha512-YjsEEL6890P7MCv6fch6Am1yq0EhQCJMXyT4LBohiu87+4/WqR7y5W3RIv53WdA901hhytgRvjlrAhibhW4qsA== -postcss-focus-within@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz#763b8788596cee9b874c999201cdde80659ef680" - integrity sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w== +postcss-image-set-function@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-6.0.0.tgz#a5aba4a805ae903ab8200b584242149c48c481fb" + integrity sha512-bg58QnJexFpPBU4IGPAugAPKV0FuFtX5rHYNSKVaV91TpHN7iwyEzz1bkIPCiSU5+BUN00e+3fV5KFrwIgRocw== dependencies: - postcss "^7.0.2" + postcss-value-parser "^4.2.0" -postcss-font-variant@^4.0.0: +postcss-initial@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz#42d4c0ab30894f60f98b17561eb5c0321f502641" - integrity sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA== - dependencies: - postcss "^7.0.2" - -postcss-gap-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz#431c192ab3ed96a3c3d09f2ff615960f902c1715" - integrity sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg== - dependencies: - postcss "^7.0.2" - -postcss-html@^0.36.0: - version "0.36.0" - resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.36.0.tgz#b40913f94eaacc2453fd30a1327ad6ee1f88b204" - integrity sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw== - dependencies: - htmlparser2 "^3.10.0" - -postcss-image-set-function@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288" - integrity sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-initial@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.4.tgz#9d32069a10531fe2ecafa0b6ac750ee0bc7efc53" - integrity sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg== - dependencies: - postcss "^7.0.2" + resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-4.0.1.tgz#529f735f72c5724a0fb30527df6fb7ac54d7de42" + integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== -postcss-lab-function@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz#bb51a6856cd12289ab4ae20db1e3821ef13d7d2e" - integrity sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg== - dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-less@^3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-3.1.4.tgz#369f58642b5928ef898ffbc1a6e93c958304c5ad" - integrity sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA== - dependencies: - postcss "^7.0.14" - -postcss-load-config@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a" - integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw== - dependencies: - cosmiconfig "^5.0.0" - import-cwd "^2.0.0" - -postcss-loader@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" - integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== +postcss-lab-function@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-6.0.1.tgz#46dc530a242972d47018c6627128d8e9e96b82c8" + integrity sha512-/Xl6JitDh7jWkcOLxrHcAlEaqkxyaG3g4iDMy5RyhNaiQPJ9Egf2+Mxp1W2qnH5jB2bj59f3RbdKmC6qx1IcXA== dependencies: - loader-utils "^1.1.0" - postcss "^7.0.0" - postcss-load-config "^2.0.0" - schema-utils "^1.0.0" + "@csstools/css-color-parser" "^1.2.2" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + "@csstools/postcss-progressive-custom-properties" "^3.0.0" -postcss-logical@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-3.0.0.tgz#2495d0f8b82e9f262725f75f9401b34e7b45d5b5" - integrity sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA== +postcss-loader@^7.3.3: + version "7.3.3" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.3.tgz#6da03e71a918ef49df1bb4be4c80401df8e249dd" + integrity sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA== dependencies: - postcss "^7.0.2" + cosmiconfig "^8.2.0" + jiti "^1.18.2" + semver "^7.3.8" -postcss-media-minmax@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" - integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== +postcss-logical@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-7.0.0.tgz#9a83426e716e3c8f957dda3fd874edbcf22c754e" + integrity sha512-zYf3vHkoW82f5UZTEXChTJvH49Yl9X37axTZsJGxrCG2kOUwtaAoz9E7tqYg0lsIoJLybaL8fk/2mOi81zVIUw== dependencies: - postcss "^7.0.2" + postcss-value-parser "^4.2.0" postcss-media-query-parser@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" integrity sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig== -postcss-modules-extract-imports@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" - integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== - dependencies: - postcss "^7.0.5" +postcss-modules-extract-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" + integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== -postcss-modules-local-by-default@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" - integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== +postcss-modules-local-by-default@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524" + integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA== dependencies: - icss-utils "^4.1.1" - postcss "^7.0.32" + icss-utils "^5.0.0" postcss-selector-parser "^6.0.2" postcss-value-parser "^4.1.0" -postcss-modules-scope@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" - integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^6.0.0" - -postcss-modules-values@^3.0.0: +postcss-modules-scope@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" - integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" + integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== dependencies: - icss-utils "^4.0.0" - postcss "^7.0.6" + postcss-selector-parser "^6.0.4" -postcss-nesting@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.1.tgz#b50ad7b7f0173e5b5e3880c3501344703e04c052" - integrity sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg== +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== dependencies: - postcss "^7.0.2" + icss-utils "^5.0.0" -postcss-overflow-shorthand@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz#31ecf350e9c6f6ddc250a78f0c3e111f32dd4c30" - integrity sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g== +postcss-nesting@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-12.0.0.tgz#729932293b925ac5bffcb6df1e2620faa0447554" + integrity sha512-knqwW65kxssmyIFadRSimaiRyLVRd0MdwfabesKw6XvGLwSOCJ+4zfvNQQCOOYij5obwpZzDpODuGRv2PCyiUw== dependencies: - postcss "^7.0.2" + "@csstools/selector-specificity" "^3.0.0" + postcss-selector-parser "^6.0.13" -postcss-page-break@^2.0.0: +postcss-opacity-percentage@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-2.0.0.tgz#add52d0e0a528cabe6afee8b46e2abb277df46bf" - integrity sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ== - dependencies: - postcss "^7.0.2" + resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-2.0.0.tgz#c0a56060cd4586e3f954dbde1efffc2deed53002" + integrity sha512-lyDrCOtntq5Y1JZpBFzIWm2wG9kbEdujpNt4NLannF+J9c8CgFIzPa80YQfdza+Y+yFfzbYj/rfoOsYsooUWTQ== -postcss-place@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-4.0.1.tgz#e9f39d33d2dc584e46ee1db45adb77ca9d1dcc62" - integrity sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg== +postcss-overflow-shorthand@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-5.0.0.tgz#1ed6d6c532cdf52b5dabec06662dc63f9207855c" + integrity sha512-2rlxDyeSics/hC2FuMdPnWiP9WUPZ5x7FTuArXLFVpaSQ2woPSfZS4RD59HuEokbZhs/wPUQJ1E3MT6zVv94MQ== dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" + postcss-value-parser "^4.2.0" -postcss-preset-env@^6.7.0: - version "6.7.1" - resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.7.1.tgz#26563d2e9395d626a45a836450844540694bfcef" - integrity sha512-rlRkgX9t0v2On33n7TK8pnkcYOATGQSv48J2RS8GsXhqtg+xk6AummHP88Y5mJo0TLJelBjePvSjScTNkj3+qw== - dependencies: - autoprefixer "^9.6.1" - browserslist "^4.6.4" - caniuse-lite "^1.0.30000981" - css-blank-pseudo "^0.1.4" - css-has-pseudo "^0.10.0" - css-prefers-color-scheme "^3.1.1" - cssdb "^4.4.0" - postcss "^7.0.17" - postcss-attribute-case-insensitive "^4.0.1" - postcss-color-functional-notation "^2.0.1" - postcss-color-gray "^5.0.0" - postcss-color-hex-alpha "^5.0.3" - postcss-color-mod-function "^3.0.3" - postcss-color-rebeccapurple "^4.0.1" - postcss-custom-media "^7.0.8" - postcss-custom-properties "^8.0.11" - postcss-custom-selectors "^5.1.2" - postcss-dir-pseudo-class "^5.0.0" - postcss-double-position-gradients "^1.0.0" - postcss-env-function "^2.0.2" - postcss-focus-visible "^4.0.0" - postcss-focus-within "^3.0.0" - postcss-font-variant "^4.0.0" - postcss-gap-properties "^2.0.0" - postcss-image-set-function "^3.0.1" - postcss-initial "^3.0.0" - postcss-lab-function "^2.0.1" - postcss-logical "^3.0.0" - postcss-media-minmax "^4.0.0" - postcss-nesting "^7.0.0" - postcss-overflow-shorthand "^2.0.0" - postcss-page-break "^2.0.0" - postcss-place "^4.0.1" - postcss-pseudo-class-any-link "^6.0.0" - postcss-replace-overflow-wrap "^3.0.0" - postcss-selector-matches "^4.0.0" - postcss-selector-not "^4.0.0" - -postcss-pseudo-class-any-link@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz#2ed3eed393b3702879dec4a87032b210daeb04d1" - integrity sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew== +postcss-page-break@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz#7fbf741c233621622b68d435babfb70dd8c1ee5f" + integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== + +postcss-place@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-9.0.0.tgz#7e47851bf40d16ce06f6013453b706100ca6c102" + integrity sha512-qLEPD9VPH5opDVemwmRaujODF9nExn24VOC3ghgVLEvfYN7VZLwJHes0q/C9YR5hI2UC3VgBE8Wkdp1TxCXhtg== dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" + postcss-value-parser "^4.2.0" -postcss-replace-overflow-wrap@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz#61b360ffdaedca84c7c918d2b0f0d0ea559ab01c" - integrity sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw== +postcss-preset-env@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-9.1.0.tgz#3e996fc261f69423a18830f7c301cb286c030d4a" + integrity sha512-G+x9BD7jb9uHBB7o720emXV00CP+VdWeirJsHC5ERSpbTd2e6Xg7vHzT+a6UkxFyddALuV+Q8wJMgeTKaau+Pg== + dependencies: + "@csstools/postcss-cascade-layers" "^4.0.0" + "@csstools/postcss-color-function" "^3.0.1" + "@csstools/postcss-color-mix-function" "^2.0.1" + "@csstools/postcss-exponential-functions" "^1.0.0" + "@csstools/postcss-font-format-keywords" "^3.0.0" + "@csstools/postcss-gradients-interpolation-method" "^4.0.1" + "@csstools/postcss-hwb-function" "^3.0.1" + "@csstools/postcss-ic-unit" "^3.0.0" + "@csstools/postcss-is-pseudo-class" "^4.0.0" + "@csstools/postcss-logical-float-and-clear" "^2.0.0" + "@csstools/postcss-logical-resize" "^2.0.0" + "@csstools/postcss-logical-viewport-units" "^2.0.1" + "@csstools/postcss-media-minmax" "^1.0.6" + "@csstools/postcss-media-queries-aspect-ratio-number-values" "^2.0.1" + "@csstools/postcss-nested-calc" "^3.0.0" + "@csstools/postcss-normalize-display-values" "^3.0.0" + "@csstools/postcss-oklab-function" "^3.0.1" + "@csstools/postcss-progressive-custom-properties" "^3.0.0" + "@csstools/postcss-relative-color-syntax" "^2.0.1" + "@csstools/postcss-scope-pseudo-class" "^3.0.0" + "@csstools/postcss-stepped-value-functions" "^3.0.1" + "@csstools/postcss-text-decoration-shorthand" "^3.0.0" + "@csstools/postcss-trigonometric-functions" "^3.0.1" + "@csstools/postcss-unset-value" "^3.0.0" + autoprefixer "^10.4.14" + browserslist "^4.21.9" + css-blank-pseudo "^6.0.0" + css-has-pseudo "^6.0.0" + css-prefers-color-scheme "^9.0.0" + cssdb "^7.7.0" + postcss-attribute-case-insensitive "^6.0.2" + postcss-clamp "^4.1.0" + postcss-color-functional-notation "^6.0.0" + postcss-color-hex-alpha "^9.0.2" + postcss-color-rebeccapurple "^9.0.0" + postcss-custom-media "^10.0.0" + postcss-custom-properties "^13.3.0" + postcss-custom-selectors "^7.1.4" + postcss-dir-pseudo-class "^8.0.0" + postcss-double-position-gradients "^5.0.0" + postcss-focus-visible "^9.0.0" + postcss-focus-within "^8.0.0" + postcss-font-variant "^5.0.0" + postcss-gap-properties "^5.0.0" + postcss-image-set-function "^6.0.0" + postcss-initial "^4.0.1" + postcss-lab-function "^6.0.1" + postcss-logical "^7.0.0" + postcss-nesting "^12.0.0" + postcss-opacity-percentage "^2.0.0" + postcss-overflow-shorthand "^5.0.0" + postcss-page-break "^3.0.4" + postcss-place "^9.0.0" + postcss-pseudo-class-any-link "^9.0.0" + postcss-replace-overflow-wrap "^4.0.0" + postcss-selector-not "^7.0.1" + postcss-value-parser "^4.2.0" + +postcss-pseudo-class-any-link@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-9.0.0.tgz#5fb5b700e0ecdc845a94eb433b8ccff756cbf660" + integrity sha512-QNCYIL98VKFKY6HGDEJpF6+K/sg9bxcUYnOmNHJxZS5wsFDFaVoPeG68WAuhsqwbIBSo/b9fjEnTwY2mTSD+uA== dependencies: - postcss "^7.0.2" + postcss-selector-parser "^6.0.13" + +postcss-replace-overflow-wrap@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz#d2df6bed10b477bf9c52fab28c568b4b29ca4319" + integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== postcss-resolve-nested-selector@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" integrity sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw== -postcss-safe-parser@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz#a6d4e48f0f37d9f7c11b2a581bf00f8ba4870b96" - integrity sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g== - dependencies: - postcss "^7.0.26" - -postcss-sass@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.4.4.tgz#91f0f3447b45ce373227a98b61f8d8f0785285a3" - integrity sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg== - dependencies: - gonzales-pe "^4.3.0" - postcss "^7.0.21" - -postcss-scss@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.1.1.tgz#ec3a75fa29a55e016b90bf3269026c53c1d2b383" - integrity sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA== - dependencies: - postcss "^7.0.6" - -postcss-selector-matches@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz#71c8248f917ba2cc93037c9637ee09c64436fcff" - integrity sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.2" +postcss-safe-parser@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1" + integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== -postcss-selector-not@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz#263016eef1cf219e0ade9a913780fc1f48204cbf" - integrity sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.2" +postcss-scss@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.6.tgz#5d62a574b950a6ae12f2aa89b60d63d9e4432bfd" + integrity sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ== -postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" - integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== +postcss-selector-not@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-7.0.1.tgz#8142e90c8eb6c8c5faecb3e9d96d4353d02e94fb" + integrity sha512-1zT5C27b/zeJhchN7fP0kBr16Cc61mu7Si9uWWLoA3Px/D9tIJPKchJCkUH3tPO5D0pCFmGeApAv8XpXBQJ8SQ== dependencies: - cssesc "^2.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" + postcss-selector-parser "^6.0.10" -postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.5: - version "6.0.11" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" - integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== +postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.13, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: + version "6.0.13" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" + integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-syntax@^0.36.2: - version "0.36.2" - resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.36.2.tgz#f08578c7d95834574e5593a82dfbfa8afae3b51c" - integrity sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w== - -postcss-value-parser@^4.1.0: +postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f" - integrity sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg== +postcss@8: + version "8.4.25" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.25.tgz#4a133f5e379eda7f61e906c3b1aaa9b81292726f" + integrity sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw== dependencies: - flatten "^1.0.2" - indexes-of "^1.0.1" - uniq "^1.0.1" + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" -postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.39" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" - integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== +postcss@^8.4.21, postcss@^8.4.25: + version "8.4.27" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" + integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== dependencies: - picocolors "^0.2.1" - source-map "^0.6.1" + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" prelude-ls@^1.2.1: version "1.2.1" @@ -9602,10 +9688,10 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -quick-lru@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" - integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== quickhull@^1.0.3: version "1.0.3" @@ -10028,6 +10114,11 @@ react-awesome-query-builder@^4.5.1: redux "^4.1.0" sqlstring "^2.3.2" +react-chartjs-2@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz#43c1e3549071c00a1a083ecbd26c1ad34d385f5d" + integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA== + react-color@^2.19.3: version "2.19.3" resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d" @@ -10250,24 +10341,24 @@ reactcss@^1.2.0: dependencies: lodash "^4.0.1" -read-pkg-up@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" - integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== +read-pkg-up@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-8.0.0.tgz#72f595b65e66110f43b052dd9af4de6b10534670" + integrity sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ== dependencies: - find-up "^4.1.0" - read-pkg "^5.2.0" - type-fest "^0.8.1" + find-up "^5.0.0" + read-pkg "^6.0.0" + type-fest "^1.0.1" -read-pkg@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" - integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== +read-pkg@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-6.0.0.tgz#a67a7d6a1c2b0c3cd6aa2ea521f40c458a4a504c" + integrity sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q== dependencies: "@types/normalize-package-data" "^2.4.0" - normalize-package-data "^2.5.0" - parse-json "^5.0.0" - type-fest "^0.6.0" + normalize-package-data "^3.0.2" + parse-json "^5.2.0" + type-fest "^1.0.1" readable-stream@^2.0.1, readable-stream@~2.3.6: version "2.3.7" @@ -10282,7 +10373,7 @@ readable-stream@^2.0.1, readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0: +readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -10305,13 +10396,13 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" -redent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" - integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== +redent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-4.0.0.tgz#0c0ba7caabb24257ab3bb7a4fd95dd1d5c5681f9" + integrity sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag== dependencies: - indent-string "^4.0.0" - strip-indent "^3.0.0" + indent-string "^5.0.0" + strip-indent "^4.0.0" redux-devtools-extension@^2.13.9: version "2.13.9" @@ -11292,11 +11383,6 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw== - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -11317,7 +11403,7 @@ resolve.exports@^2.0.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.1.tgz#cee884cd4e3f355660e501fa3276b27d7ffe5a20" integrity sha512-OEJWVeimw8mgQuj3HfkNl4KqRevH7lzeQNaWRPfx0PPse7Jk6ozcsG4FKVgtzDsC1KUF+YlTHh17NcgHOPykLw== -resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.9.0: +resolve@^1.12.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.9.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -11476,15 +11562,6 @@ scheduler@^0.19.1: loose-envify "^1.1.0" object-assign "^4.1.1" -schema-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" - schema-utils@^2.6.5, schema-utils@^2.7.0: version "2.7.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" @@ -11532,22 +11609,22 @@ selfsigned@^2.0.1: dependencies: node-forge "^1" -"semver@2 || 3 || 4 || 5", semver@^5.6.0, semver@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== +semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" +semver@^5.6.0, semver@^5.7.1: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@~7.0.0: version "7.0.0" @@ -11661,6 +11738,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" + integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== + simple-concat@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" @@ -11747,7 +11829,7 @@ sockjs@^0.3.24: uuid "^8.3.2" websocket-driver "^0.7.4" -"source-map-js@>=0.6.2 <2.0.0": +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== @@ -11851,11 +11933,6 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" -specificity@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019" - integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg== - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -11923,7 +12000,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12043,12 +12120,12 @@ strip-final-newline@^3.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== -strip-indent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" - integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== +strip-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-4.0.0.tgz#b41379433dd06f5eae805e21d631e07ee670d853" + integrity sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA== dependencies: - min-indent "^1.0.0" + min-indent "^1.0.1" strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" @@ -12082,78 +12159,90 @@ style-to-object@^0.4.0: dependencies: inline-style-parser "0.1.1" -stylelint-config-recommended@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-3.0.0.tgz#e0e547434016c5539fe2650afd58049a2fd1d657" - integrity sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ== +stylelint-config-recommended-scss@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-12.0.0.tgz#9d9e82c46012649f11bfebcbc788f58e61860f33" + integrity sha512-5Bb2mlGy6WLa30oNeKpZvavv2lowJUsUJO25+OA68GFTemlwd1zbFsL7q0bReKipOSU3sG47hKneZ6Nd+ctrFA== + dependencies: + postcss-scss "^4.0.6" + stylelint-config-recommended "^12.0.0" + stylelint-scss "^5.0.0" + +stylelint-config-recommended@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-12.0.0.tgz#d0993232fca017065fd5acfcb52dd8a188784ef4" + integrity sha512-x6x8QNARrGO2sG6iURkzqL+Dp+4bJorPMMRNPScdvaUK8PsynriOcMW7AFDKqkWAS5wbue/u8fUT/4ynzcmqdQ== + +stylelint-config-standard-scss@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-standard-scss/-/stylelint-config-standard-scss-10.0.0.tgz#159a54a01b80649bf0143fa7ba086b676a1a749e" + integrity sha512-bChBEo1p3xUVWh/wenJI+josoMk21f2yuLDGzGjmKYcALfl2u3DFltY+n4UHswYiXghqXaA8mRh+bFy/q1hQlg== + dependencies: + stylelint-config-recommended-scss "^12.0.0" + stylelint-config-standard "^33.0.0" -stylelint-config-standard@^20.0.0: - version "20.0.0" - resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-20.0.0.tgz#06135090c9e064befee3d594289f50e295b5e20d" - integrity sha512-IB2iFdzOTA/zS4jSVav6z+wGtin08qfj+YyExHB3LF9lnouQht//YyB0KZq9gGz5HNPkddHOzcY8HsUey6ZUlA== +stylelint-config-standard@^33.0.0: + version "33.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-33.0.0.tgz#1f7bb299153a53874073e93829e37a475842f0f9" + integrity sha512-eyxnLWoXImUn77+ODIuW9qXBDNM+ALN68L3wT1lN2oNspZ7D9NVGlNHb2QCUn4xDug6VZLsh0tF8NyoYzkgTzg== dependencies: - stylelint-config-recommended "^3.0.0" + stylelint-config-recommended "^12.0.0" -stylelint@^13.6.1: - version "13.13.1" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.13.1.tgz#fca9c9f5de7990ab26a00f167b8978f083a18f3c" - integrity sha512-Mv+BQr5XTUrKqAXmpqm6Ddli6Ief+AiPZkRsIrAoUKFuq/ElkUh9ZMYxXD0iQNZ5ADghZKLOWz1h7hTClB7zgQ== +stylelint-scss@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-5.0.1.tgz#b33a6580b5734eace083cfc2cc3021225e28547f" + integrity sha512-n87iCRZrr2J7//I/QFsDXxFLnHKw633U4qvWZ+mOW6KDAp/HLj06H+6+f9zOuTYy+MdGdTuCSDROCpQIhw5fvQ== dependencies: - "@stylelint/postcss-css-in-js" "^0.37.2" - "@stylelint/postcss-markdown" "^0.36.2" - autoprefixer "^9.8.6" + postcss-media-query-parser "^0.2.3" + postcss-resolve-nested-selector "^0.1.1" + postcss-selector-parser "^6.0.13" + postcss-value-parser "^4.2.0" + +stylelint@^15.10.2: + version "15.10.2" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.10.2.tgz#0ee5a8371d3a2e1ff27fefd48309d3ddef7c3405" + integrity sha512-UxqSb3hB74g4DTO45QhUHkJMjKKU//lNUAOWyvPBVPZbCknJ5HjOWWZo+UDuhHa9FLeVdHBZXxu43eXkjyIPWg== + dependencies: + "@csstools/css-parser-algorithms" "^2.3.0" + "@csstools/css-tokenizer" "^2.1.1" + "@csstools/media-query-list-parser" "^2.1.2" + "@csstools/selector-specificity" "^3.0.0" balanced-match "^2.0.0" - chalk "^4.1.1" - cosmiconfig "^7.0.0" - debug "^4.3.1" - execall "^2.0.0" - fast-glob "^3.2.5" - fastest-levenshtein "^1.0.12" + colord "^2.9.3" + cosmiconfig "^8.2.0" + css-functions-list "^3.2.0" + css-tree "^2.3.1" + debug "^4.3.4" + fast-glob "^3.3.0" + fastest-levenshtein "^1.0.16" file-entry-cache "^6.0.1" - get-stdin "^8.0.0" global-modules "^2.0.0" - globby "^11.0.3" + globby "^11.1.0" globjoin "^0.1.4" - html-tags "^3.1.0" - ignore "^5.1.8" + html-tags "^3.3.1" + ignore "^5.2.4" import-lazy "^4.0.0" imurmurhash "^0.1.4" - known-css-properties "^0.21.0" - lodash "^4.17.21" - log-symbols "^4.1.0" + is-plain-object "^5.0.0" + known-css-properties "^0.27.0" mathml-tag-names "^2.1.3" - meow "^9.0.0" - micromatch "^4.0.4" - normalize-selector "^0.2.0" - postcss "^7.0.35" - postcss-html "^0.36.0" - postcss-less "^3.1.4" - postcss-media-query-parser "^0.2.3" + meow "^10.1.5" + micromatch "^4.0.5" + normalize-path "^3.0.0" + picocolors "^1.0.0" + postcss "^8.4.25" postcss-resolve-nested-selector "^0.1.1" - postcss-safe-parser "^4.0.2" - postcss-sass "^0.4.4" - postcss-scss "^2.1.1" - postcss-selector-parser "^6.0.5" - postcss-syntax "^0.36.2" - postcss-value-parser "^4.1.0" + postcss-safe-parser "^6.0.0" + postcss-selector-parser "^6.0.13" + postcss-value-parser "^4.2.0" resolve-from "^5.0.0" - slash "^3.0.0" - specificity "^0.4.1" - string-width "^4.2.2" - strip-ansi "^6.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" style-search "^0.1.0" - sugarss "^2.0.0" + supports-hyperlinks "^3.0.0" svg-tags "^1.0.0" - table "^6.6.0" - v8-compile-cache "^2.3.0" - write-file-atomic "^3.0.3" - -sugarss@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-2.0.0.tgz#ddd76e0124b297d40bf3cca31c8b22ecb43bc61d" - integrity sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ== - dependencies: - postcss "^7.0.2" + table "^6.8.1" + write-file-atomic "^5.0.1" supports-color@^2.0.0: version "2.0.0" @@ -12174,7 +12263,7 @@ supports-color@^6.0.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: +supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -12188,6 +12277,14 @@ supports-color@^8.0.0: dependencies: has-flag "^4.0.0" +supports-hyperlinks@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz#c711352a5c89070779b4dad54c05a2f14b15c94b" + integrity sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -12263,7 +12360,7 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -table@^6.0.9, table@^6.6.0: +table@^6.0.9, table@^6.8.1: version "6.8.1" resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== @@ -12414,9 +12511,9 @@ touch@^3.1.0: nopt "~1.0.10" tough-cookie@^4.0.0, tough-cookie@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" - integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== + version "4.1.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" + integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== dependencies: psl "^1.1.33" punycode "^2.1.1" @@ -12440,10 +12537,10 @@ trim-lines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== -trim-newlines@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" - integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== +trim-newlines@^4.0.2: + version "4.1.1" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.1.1.tgz#28c88deb50ed10c7ba6dc2474421904a00139125" + integrity sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ== trim-right@^1.0.1: version "1.0.1" @@ -12533,11 +12630,6 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.18.0: - version "0.18.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" - integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -12553,16 +12645,16 @@ type-fest@^0.3.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== -type-fest@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" - integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== - -type-fest@^0.8.0, type-fest@^0.8.1: +type-fest@^0.8.0: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^1.0.1, type-fest@^1.2.1, type-fest@^1.2.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -12717,11 +12809,6 @@ unified@^9.1.0: trough "^1.0.0" vfile "^4.0.0" -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA== - unist-builder@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-3.0.0.tgz#728baca4767c0e784e1e64bb44b5a5a753021a04" @@ -12738,13 +12825,6 @@ unist-util-filter@^4.0.0: unist-util-is "^5.0.0" unist-util-visit-parents "^5.0.0" -unist-util-find-all-after@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-3.0.2.tgz#fdfecd14c5b7aea5e9ef38d5e0d5f774eeb561f6" - integrity sha512-xaTC/AGZ0rIM2gM28YVRAFPIZpzbpDtU3dRmp7EXlNVA8ziQc4hY3H7BHXM1J49nEmiqc3svnqMReW+PGqbZKQ== - dependencies: - unist-util-is "^4.0.0" - unist-util-generated@^1.0.0, unist-util-generated@^1.1.0: version "1.1.6" resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b" @@ -12872,6 +12952,14 @@ unquote@~1.1.1: resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" integrity sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg== +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + update-browserslist-db@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" @@ -12947,7 +13035,7 @@ uvu@^0.5.0: kleur "^4.0.3" sade "^1.7.3" -v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: +v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== @@ -13335,9 +13423,9 @@ wildcard@^2.0.0: integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== word-wrap@^1.2.3, word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" + integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== worker-loader@^3.0.8: version "3.0.8" @@ -13378,7 +13466,7 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: +write-file-atomic@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== @@ -13396,6 +13484,14 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" +write-file-atomic@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" + integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^4.0.1" + ws@^8.11.0: version "8.13.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" @@ -13441,11 +13537,6 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - yaml@^2.1.3: version "2.2.1" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.1.tgz#3014bf0482dcd15147aa8e56109ce8632cd60ce4" @@ -13459,7 +13550,7 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.3: +yargs-parser@^20.2.9: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==